#!/usr/bin/perl
#
# Support .gitslave files, recursive processing of git commands into slave directories
#
# ++Copyright LIBBK++
#
# Copyright (c) 2003 The Authors. All rights reserved.
#
# This source code is licensed to you under the terms of the file
# LICENSE.TXT in this release for further details.
#
# Mail <projectbaka@baka.org> for further information
#
# --Copyright LIBBK--
#
use strict;
use warnings;
no warnings 'uninitialized';
no warnings 'io';
use Config;
use Cwd;
use File::Spec;
use constant GITSLAVE => '.gitslave';
use Getopt::Long;
use File::Path;
use File::Basename;

my $eval_errs = '';
my ($want_parallel) = (0);
eval 'use Parallel::Iterator;';
if ($@)
{
  undef($want_parallel);
  my $err = $@;
  $err =~ s/\(\@INC [^)]*\)//g;
  $err =~ s/ at .* line \d+\.//g;
  $eval_errs .= $err;
}
my ($want_progress,$progress_count) = (0,0);
eval 'use Term::ProgressBar;';
if ($@)
{
  undef($want_progress);
  my $err = $@;
  $err =~ s/\(\@INC [^)]*\)//g;
  $err =~ s/ at .* line \d+\.//g;
  $eval_errs .= $err;
}
my ($progress,$missingcount,$foundcount);

our($GITSLAVE) = $ENV{'GITSLAVE'}||GITSLAVE;
our($GITS_DIR);
my($TOP);
our(@slaves);
my($USAGE) = "Usage: gits [-p|--parallel COUNT] [-v|--verbose]+ [--quiet] [--rawout] [--help] [--version]\n".
"\t[-n|--no-pager] [--paginate] [--eval-args] [--exclude SLAVE-REGEXP]\n".
"\t[--keep-going] [--no-commit] [--no-hide] [--no-progress]\n".
"\t[--no-master] [--with-ifpresent|--just-ifpresent] SUBCOMMAND [ARGS]...\n";
our(%OPTIONS);
my ($returncode) = 0;
my $pagination;
delete($ENV{'GIT_DIR'});
our($fromcheckout);
our($git) = 'git';

# Some people are stupid enough to set CDPATH and that screws up the formatting
delete($ENV{'CDPATH'});



######################################################################
#
# Translate wait status ($?) into human-readable terms
#
sub exitcode(;$)
{
  my ($ret) = @_;
  $ret = $? if !defined($ret);

  my $sig = $ret & 127;

  # shell exits with 128 + signal for child killed by signal
  $sig = ($ret >> 8) - 128 if (!$sig && ($ret >> 8) > 128);

  if ($sig)
  {
    my $signame = (split(' ', $Config{sig_name}))[$sig];
    return "killed by SIG" . $signame . " (signal " . $sig . ")";
  }

  return "exit " . ($ret >> 8);
}



######################################################################
#
# Perform substitution of path and directory components
#
sub gitsubst($$)
{
  my ($data,$slave) = @_;

  my ($pattern) = quotemeta('%%dir%%');
  if ($data =~ /$pattern/)
  {
    $data =~ s/$pattern/$slave/g;
  }

  $pattern = quotemeta('%%path%%');
  if ($data =~ /$pattern/)
  {
    my $subst = Cwd::realpath($GITS_DIR."/".$slave);
    $data =~ s/$pattern/$subst/g;
  }

  $pattern = quotemeta('%%basename%%');
  if ($data =~ /$pattern/)
  {
    my $subst = basename(Cwd::realpath($GITS_DIR."/".$slave));
    $data =~ s/$pattern/$subst/g;
  }

  $pattern = quotemeta('%%upstream%%');
  if ($data =~ /$pattern/)
  {
    my $subst = scalar(`cd "$slave" && $git config remote.origin.url`);
    chomp($subst);
    $data =~ s/$pattern/$subst/g;
  }

  $pattern = quotemeta('%%upstream_base%%');
  if ($data =~ /$pattern/)
  {
    my $subst = basename(scalar(`cd "$slave" && $git config remote.origin.url`));
    chomp($subst);
    $data =~ s/$pattern/$subst/g;
  }

  $data;
}



######################################################################
#
# Run a command where we don't care about the output
#
sub docmd($)
{
  my ($cmd) = @_;

  if ($OPTIONS{'quiet'})
  {
    $cmd = "{ $cmd ; } 2>/dev/null";
  }
  elsif ($pagination)
  {
    $cmd = "{ $cmd ; } 2>&1";
  }
  print STDERR "Running command: `$cmd`\n" if ($OPTIONS{'verbose'} > 1);
  system($cmd);
  print STDERR "Command " . exitcode() . "\n" if ($OPTIONS{'verbose'} > 2);
  $? == 0;
}



######################################################################
#
# Run a command, retrieving the output
#
sub getcmd($;$)
{
  my ($cmd,$slave) = @_;

  $cmd = gitsubst($cmd,$slave) if ($slave);

  $cmd = "{ $cmd ;} 2>&1";
  print STDERR "Running command: `$cmd`\n" if ($OPTIONS{'verbose'} > 1);
  my $out = `$cmd`;
  # strip out progress messages (e.g. "Writing objects:  94% (47/50)")
  $out =~ s/^.*\d% .*\r(?!\n)//mg;
  if ($OPTIONS{'verbose'} > 2)
  {
    my $tout = $out;
    chomp($tout);
    $tout =~ s/\n/|\n |/g;
    print STDERR "Command " . exitcode() . "\n |$tout|\n";
  }

  wantarray?($?, $out):$out;
}



######################################################################
#
# Find the repository URL for this remote
#
sub find_remote_repo($$)
{
  my ($remote,$base) = @_;

  my ($ret, $master_repo) = getcmd(qq^cd "$base" && $git config "remote.$remote.url"^);
  chomp($master_repo);
  die "Could not find git upstream clone which this is relative to ($base $remote)\n" unless ($ret == 0 && $master_repo);
  $master_repo;
}




######################################################################
#
# Find the absolute path to repository (relatives are relative to upstream repository)
#
# We need to handle the following schemes:
# scheme://domain/~user
# scheme://domain/~user/dir
# scheme://domain/path
# scheme://domain:port/path
# scheme://username:password@domain:port/path
#
# where scheme == http ssh git git+ssh
# where /path == /path/from/root/to/repo
# where /path == /repo
# where /path == /~user
# where /path == /~user/repo
# domain:path
#
# username@domain:path
#
# where path == repo
# where path == /path/to/repo
# where path == path/to/repo
# where path == ~repo
# where path == ~user/repo
#
# Note that git currently cannot clone from the following directory:
# /tmp/foo/bar:bar/baz Any : will cause the system to treat it like a
# hostname or URL scheme as above.
#
# The relative URL schemes we support include:
# ../lib1
# ^/src/repos/lib1
#
sub resolve_repository($$;$$$$)
{
  my ($relrepos,$relative,$remote,$absrepos,$localbase,$myfromcheckout) = @_;

  $remote = "origin" unless ($remote);

  $myfromcheckout = $fromcheckout unless defined($myfromcheckout);
  $myfromcheckout = get_fromcheckout($remote) unless defined($myfromcheckout);

  $localbase = "." if (!$localbase || $myfromcheckout);

  if ($myfromcheckout)
  {
    $relrepos = $relative;
  }

  # Check for already absolute
  return $relrepos if ($relrepos =~ m-^(/|.+?:)-);

  # Find upstream origin URL
  $absrepos = find_remote_repo($remote,$localbase) if (!$absrepos || ($localbase ne "." && $localbase ne $GITS_DIR));

  # Nuke trailing .git, if necessary
  $absrepos =~ s:/\.git$::;

  ##################################################
  # Break into method and path sections
  my ($rprefix, $rsuffix) = ("", $absrepos);
  my ($lprefix, $lsuffix) = ("", $relrepos);

  if ($absrepos =~ /(.+?:)(.*)/)
  {
    $rprefix = $1;
    $rsuffix = $2;

    # If this is scheme://hostish/ move /hostish into prefix.
    if ($rsuffix =~ s:^(//[^/]*)/:/:)
    {
      $rprefix .= $1;
    }
  }

  if ($relrepos =~ /(.+?:)(.*)/)
  {
    $lprefix = $1;
    $lsuffix = $2;

    # If this is scheme://hostish/ move /hostish into prefix.
    if ($lsuffix =~ s:^(//[^/]*)/:/:)
    {
      $lprefix .= $1;
    }
  }

  # At this point, prefix has all networkish components, and suffix
  # has all hostish location components.  We don't know if suffix is
  # absolute or "relative" at this point.

  # Handle ^/src/repos/lib1 case
  if ($lsuffix =~ m:^\^(.*):)
  {
    return (($lprefix || $rprefix).$1);
  }

  my $newpath = "$rsuffix/$lsuffix";

  # Handle "xxx//yyy" -> "xxx/yyy"
  while ($newpath =~ s://:/:) {;}

  # Handle "xxx/./yyy" -> "xxx/yyy"
  while ($newpath =~ s:/\./:/:) {;}
  # Handle "./xxx" -> "xxx"
  while ($newpath =~ s:^\./::) {;}
  # Handle "/." -> "/"
  while ($newpath =~ s:^/\.$:/:) {;}
  # Handle "xxx/." -> "xxx"
  while ($newpath =~ s:/\.$::) {;}

  # Handle "/xxx/../yyy" -> "/yyy"
  # Handle "xxx/../yyy" -> "yyy"
  # Handle "/xxx/.." -> "/"
  # Handle "xxx/.." -> ""
  while ($newpath =~ s:(^ | /) [^/]+ / \.\. ($ | /):$1:x) {;}

  $relrepos = ($lprefix || $rprefix).$newpath;
}



######################################################################
#
# check whether command requires slave configuration
#
sub ephemeral_command($;$)
{
  my ($cmd,$depth) = @_;

  if ($cmd eq "prepare" or $cmd eq "resolve" or $cmd eq "clone" or $cmd eq "help" or $cmd eq "release" or ($depth && $cmd eq "populate"))
  {
    $missingcount++;
    return 1;
  }
  return 0;
}



######################################################################
#
# Recursive slave list population
#
sub git_slave_list_recursive($$$$$$$);
sub git_slave_list_recursive($$$$$$$)
{
  my ($file,$gitsp,$seenp,$cmd,$base,$vpresentp,$depth) = @_;

  my ($r);

  $foundcount++;

  if (!open($r, "<", $file))
  {
    die "Could not find $file file\n"
      unless (!defined($cmd) or ephemeral_command($cmd,$depth));
    return;
  }
  my ($lineno) = 0;
  while (<$r>)
  {
    s/\r$//g;
    $lineno++;
    if (/^\#include\s+\"([^\"]+)\"\s*(\sifpresent)?$/)
    {
      my ($slavefile,$flags) = ($1,$2);
      my $slave = $slavefile;
      $slave =~ s:^(.*)/.*:$1:;
      $slavefile = "$base/$slavefile" unless ($slavefile =~ m:^/:);
      my $newbase = $slavefile;
      $newbase =~ s:^(.*)/.*:$1:;
      my $newifpresent = 1
	if (map($_->[1] eq $slave && $_->[3] =~ /ifpresent/, @$gitsp));
      if ($flags =~ /ifpresent/ && !$OPTIONS{'with-ifpresent'} && !$OPTIONS{'just-ifpresent'})
      {
	next unless (-f $slavefile);
      }
      # Disable --just-ifpresent (and enable --with-ifpresent) for recursive
      # superprojects with ifpresent (on either slave _or_ #include statement)
      # - this is important for gits release to make sure that we don't rm -rf
      # ifpresent slave superproject without checking state of all its slaves.
      my $justifpresent =
	($newifpresent || $flags =~ /ifpresent/) && $OPTIONS{'just-ifpresent'};
      if ($justifpresent)
      {
	delete $OPTIONS{'just-ifpresent'};
	$OPTIONS{'with-ifpresent'} = 1;
      }
      git_slave_list_recursive($slavefile,$gitsp,$seenp,$cmd,$newbase,$vpresentp,$depth+1);
      # no need to delete --with-ifpresent as --just-ifpresent subsumes it
      $OPTIONS{'just-ifpresent'} = 1 if ($justifpresent);
      next;
    }
    next if (/^\s*(?:\#.*)$/);
    next if ($OPTIONS{'exclude'} && /$OPTIONS{'exclude'}/);
    die "Bad line $lineno in $file\n" unless (/^\"(.*)\" \"(.*)\"( ifpresent)?$/);
    my ($baserel,$ckoutrel,$flags) = ($1,$2,$3);

    # add to slaves if explicitly listed as argument, or,
    # if --just-ifpresent, then if ifpresent flag,
    # otherwise if not ifpresent flag or --with-ifpresent or actually present
    if ($vpresentp->{$ckoutrel} ||
	($OPTIONS{'just-ifpresent'} ? $flags =~ /ifpresent/ :
	 ($flags !~ /ifpresent/ || $OPTIONS{'with-ifpresent'} ||
	  -d "$base/$ckoutrel/.")))
    {
      my ($relbase) = $base;
      my ($qbase) = quotemeta($GITS_DIR);
      $relbase =~ s:^$qbase/*::;
      $relbase = "$relbase/" if ($relbase);

      $ckoutrel = "$relbase$ckoutrel";

      push(@$gitsp,[$baserel, $ckoutrel, $base, $flags])
	if (!$seenp->{$ckoutrel});
      $seenp->{$ckoutrel} = 1;
    }
  }
  close($r);
}



######################################################################
#
# Get list of currently configured slaves
#
sub git_slave_list($;@)
{
  my ($cmd,@virtualpresent) = @_;
  my (@gits);
  my (%seen);
  my (%vpresent);
  map($vpresent{$_}=1,@virtualpresent);

  $missingcount = $foundcount = 0;

  foreach my $slavefile (split(/, /,$GITSLAVE))
  {
    $slavefile = "$GITS_DIR/$slavefile" unless ($slavefile =~ m:^/:);
    git_slave_list_recursive($slavefile,\@gits,\%seen,$cmd,$GITS_DIR,\%vpresent, 0);
  }
  @gits;
}



######################################################################
#
# Configure persistent fromcheckout
#
sub get_fromcheckout($)
{
  `$git config 'gits.$_[0].fromcheckout'`;
}



######################################################################
#
# Configure persistent fromcheckout
#
sub set_fromcheckout($$)
{
  if ($_[0])
  {
    `$git config 'gits.$_[1].fromcheckout' 1`;
    $fromcheckout = 1;
  }
  else
  {
    `$git config --unset 'gits.$_[1].fromcheckout'`;
    $fromcheckout = 0;
  }
  $? == 0;
}



######################################################################
#
# Check for presence of slave
#
my ($warngitslavesync) = 0;

sub missing_slave($)
{
  my ($slave) = @_;
  if (!-d $slave)
  {
    warn "Missing one or more git slaves including $slave, consider 'gits populate'.\n" unless ($warngitslavesync || $OPTIONS{'quiet'});
    $warngitslavesync++;
    return 1;
  }
  return 0;
}



######################################################################
#
# Add shell quoting to avoid problems
#
sub quoteit(@)
{
  my $str;
  foreach my $item (@_)
  {
    my ($i) = $item;
    my $o;
    my $len = length($i);

    foreach(my $x=0;$x<$len;$x++)
    {
      my $c = substr($i,$x,1);
      if ($OPTIONS{'eval-args'})
      {
	$o .= '\\' if ($c =~ /[\\\"]/);
      }
      else
      {
	$o .= '\\' if ($c =~ /[\\\$\`\"]/);
      }
      $o .= $c;
    }
    $str .= '"'.$o.'" ';
  }
  chop($str);
  $str;
}



######################################################################
#
# Populate - clone projects listed in .gitslaves
#
sub do_populate(;$$);
sub do_populate(;$$)
{
  my ($nocheckout,$reference)=@_;
  my $curbranch;
  our %hooked;
  my $wanthooks;

  my ($ret,$out) = getcmd(qq^$git config gits.nohooks^);
  $wanthooks = !$out && -d "git-hooks";

  $nocheckout='-n' if ($nocheckout);

  foreach my $group (@slaves)
  {
    my $slave = $group->[1];

    $curbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd("$git branch --no-color"))))[0] unless ($curbranch);

    if (! -d $slave || (! -f "$slave/config" && ! -d "$slave/.git"))
    {
      print STDERR "gits: Cloning $slave\n" if ($OPTIONS{'verbose'});
      my ($repos) = resolve_repository($group->[0],$group->[1],undef,undef,$group->[2]);
      my ($refarg) = "";
      if ($reference)
      {
	$refarg = "--reference ".quoteit(resolve_repository($group->[0],$group->[1],undef,$reference->[0],$group->[2],$reference->[1]));
      }
      docmd(qq^$git clone $refarg $nocheckout "$repos" "$slave"^) || die "Could not clone $repos onto $slave\n";

      ($ret, $out) = getcmd(qq^cd "$slave" && $git branch --no-color | sort^);

      my $subbranch = (grep(s/\s*\*\s+//, split(/\n/,$out)))[0];
      if ($curbranch ne $subbranch && $curbranch !~ /no branch/)
      {
	print qq^Switching "$slave" to branch "$curbranch"\n^;

	# <TRICKY>Use of -f for git checkout is dangerous, as it throws away
	# local changes; but it is needed here for git 1.6, which treats the
	# empty (but not declared bare!) working tree as an uncommitted change
	# deleting all files that prevents checkout of the new branch</TRICKY>
	my $force = "";
	$force = " -f" if ($nocheckout);
	docmd(qq^cd "$slave" && $git checkout$force -b $curbranch origin/$curbranch^) || die "Branch inconsistency, branch $curbranch does not exist for $slave\n";
      }
    }
    else
    {
      ($ret, $out) = getcmd(qq^cd "$slave" && $git branch --no-color | sort^);

      # handle case of failed fetch during clone (e.g. network connectivity)
      docmd(qq^cd "$slave" && git fetch^) || die "Could not fetch remote\n"
	if (!$out);

      my ($cursubbranch,$subbranch) = grep(s/^\s*\*\s+(.*)|\s+($curbranch)$/$1$2/, split(/\n/,$out));
      if ($curbranch ne $cursubbranch && $curbranch !~ /no branch/ && !$nocheckout)
      {
	print qq^Switching "$slave" to branch "$curbranch"\n^;

	my $gitversion = `$git --version`;
	if ($curbranch ne $subbranch && $gitversion =~ /git version (1\.[12345]\.|1\.6\.[012345])/)
	{
	  docmd(qq^cd "$slave" && $git checkout -b $curbranch origin/$curbranch^) || die "Branch inconsistency, branch $curbranch does not exist for $slave\n";
	}
	else
	{
	  docmd(qq^cd "$slave" && $git checkout $curbranch^) || die "Branch inconsistency, branch $curbranch does not exist for $slave\n";
	}
      }
      else
      {
	my $branchmsg = "";
	$branchmsg = " (branch $curbranch)" if ($curbranch ne "master");
	print STDERR "gits: $slave already exists\n" if ($OPTIONS{'verbose'});
      }
    }

    # Count the number of directory separators for deep slave support
    my @slaves = split(m:/:,$slave);
    my ($updir) = "../..".("/.."x @slaves);

    # Update hooks from master (-regex '.*\\*\$' matches bogus symlink '*')
    docmd(qq^cd "$slave/.git/hooks" && { find . -type l -a '(' -lname '$updir/git-hooks*' -o -regex '.*\\*\$' ')' -exec rm -f '{}' ';'; find $updir/git-hooks/ -type f -exec sh -c 'ln -s \$0 .' '{}' ';'; }^)
      if (!$hooked{"$slave"} && $wanthooks);
    $hooked{"$slave"} = 1;
  }
  # Update master's hooks as well
  docmd(qq^cd .git/hooks && { find . -type l -a '(' -lname '../../git-hooks*' -o -regex '.*\\*\$' ')' -exec rm -f '{}' ';'; find ../../git-hooks/ -type f -exec sh -c 'ln -s \$0 .' '{}' ';'; }^)
    if (!$hooked{"."} && $wanthooks);
  $hooked{"."} = 1;

  if ($missingcount)
  {
    my ($oldfoundcount) = $foundcount;
    @slaves = git_slave_list($ARGV[0]);
    git_slave_list("errorme") if ($missingcount && $foundcount <= $oldfoundcount);
    do_populate($nocheckout,$reference);
  }
}



######################################################################
#
# git checkout, gits style
#
# pushprocessing: 0 -- want progress bar
# pushprocessing: 1 -- print informational messages, revert to oldbranch on die
#			(oldbranch is first argument, popped from @args)
# pushprocessing: 2 -- do not abort on errors - return warning
#
sub do_checkout($@);
sub do_checkout($@)
{
  my ($pushprocessing,@args) = @_;
  my ($oldbranch);
  my ($ret, $msg) = (0, undef);
  my ($slavecnt) = -1;
  my ($ok, $okcnt);
  my (%msg);
  my ($group, $slave, $repo) = (undef, ".", $TOP);

  $oldbranch = shift(@args) if ($pushprocessing == 1);

  if (!$OPTIONS{'no-master'})
  {
    my ($cmd) = qq^cd "%%dir%%" && $git checkout ^.quoteit(@args);
    $cmd .= " --" if ($pushprocessing);
    $cmd .= " >/dev/null 2>&1" if ($pushprocessing == 2);
    ($ret, $msg) = getcmd($cmd,$slave);

    if ($ret == 0)
    {
      # Reload list of slaves, which might have changed due to superproject checkout
      @slaves = git_slave_list("checkout");

      # New list of slaves might have stuff we need to check out
      do_populate(1);
    }
  }
  my ($totcnt) = $#slaves + ($OPTIONS{'no-master'} ? 1 : 2);
  my ($progress) = Term::ProgressBar->new({remove=>1, count=>($#slaves+1)}) if ($want_progress && !$pushprocessing);
  $progress->max_update_rate(1) if ($progress);

  # Tricky: This backwards loop is designed so that the return code processing can be shared between superproject and slaves
  my $failed;
  do
  {
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits checkout (@{[join(' ',@args)]}), failed for: '$repo': " . exitcode($ret);
      if ($pushprocessing > 1)
      {
	$failed .= " $repo";
      }
      elsif ($OPTIONS{'keep-going'})
      {
	$warn = "Error in $warn (continuing)\n $msg\n";
	$warn = "\n$warn" if ($progress);
	warn $warn;
      }
      else
      {
	my $diemsg = "Aborting $warn\n ";
	$diemsg = "\n$diemsg" if ($progress);
	if ($ok)
	{
	  $diemsg .= "(ran fine on: $ok, did not run on @{[$totcnt-$okcnt]} other(s); no rollbacks)";
	}
	else
	{
	  $diemsg .= "(first entry, no other successfully executed)";
	}
	# <TODO>try to restore old branch, if desirable/possible</TODO>
	$msg .= do_checkout(2, $oldbranch) if ($pushprocessing == 1);
	die "$diemsg\n  $msg\n";
      }
    }
    else
    {
      $ok .= " $repo";
      $okcnt++;
      if ($pushprocessing)
      {
	$msg =~ s/Switched to branch.*\n//;
	$msg =~ s/Already on \".*\n//;
	push(@{$msg{"$args[0]\@$repo"}}, $repo);
      }
      else
      {
	push(@{$msg{$msg}}, $repo);
      }
    }

    # Forward through the slave list
    if (++$slavecnt <= $#slaves)
    {
      $group = $slaves[$slavecnt];
      $repo = $slave = $group->[1];

      my ($cmd) = qq^cd "%%dir%%" && $git checkout ^.quoteit(@args);
      $cmd .= " --" if ($pushprocessing);
      $cmd .= " >/dev/null 2>&1" if ($pushprocessing == 2);
      ($ret, $msg) = getcmd($cmd,$slave);
    }

    $progress->update($slavecnt) if ($progress);
  } while ($slavecnt <= $#slaves);

  if ($pushprocessing > 1)
  {
    my $msg = "\nUnable to checkout original $args[0] for$failed" if ($failed);
    $msg .= " (successful for $okcnt other(s))" if ($failed);
    return $msg;
  }

  %msg;
}



######################################################################
#
# Find .gitignore for a given path
#
sub findignore($)
{
  my ($mntpt) = @_;

  my $gitignore = $GITS_DIR;
  my $ignorefile = $mntpt;
  if ($mntpt =~ m:.*/.*:)
  {
    my @dirs = split(m:/:,$mntpt);
    # print STDERR "GITIGNORE $gitignore $mntpt $ignorefile $#dirs\n";
    for(my $x=$#dirs-1;$x>=0;$x--)
    {
      my ($tmp) = $GITS_DIR."/".join("/",@dirs[0 .. $x]);
      if (-d "$tmp/.git")
      {
	$gitignore = $tmp;
	$ignorefile = join("/",@dirs[$x+1 .. $#dirs]);
	print STDERR "Found intervening .git, want to use $gitignore dir with content $ignorefile\n" if ($OPTIONS{'verbose'} > 1);
	last;
      }
    }
  }
  ($gitignore,$ignorefile);
}



######################################################################
#
# Standard gits output summmarization routine
#
sub stdout($;$)
{
  my ($msgp,$suppressempty) = @_;
  my $keys = scalar(keys(%{$msgp}));
  my $other = " other";
  my $limit = 7;

  # If all commands return identical strings, generate single header line
  # (unless we are in verbose mode) - suppress all output if all are empty
  if (!$OPTIONS{'verbose'} && $keys == 1)
  {
    return if (defined($msgp->{""}));
    $other = "";
  }

  foreach my $msg (sort { $#{$msgp->{$a}} <=> $#{$msgp->{$b}} } keys %{$msgp})
  {
    if ($OPTIONS{'rawout'})
    {
      foreach my $repos (@{$msgp->{$msg}})
      {
	print $msg;
      }
      next;
    }
    $keys--;
    if ($suppressempty && !$OPTIONS{'verbose'} && $msg eq "")
    {
      $limit = 999999;		# "other" not well-defined if some are omitted
      next;
    }
    my $repos = $#{$msgp->{$msg}};
    if ($OPTIONS{'verbose'} == 0 && !$keys && $repos > $limit)
    {
      print "On all$other repositories:\n";
    }
    else
    {
      $limit = $repos + 4 if ($repos + 4 > $limit);
      print "On: ", join(', ',@{$msgp->{$msg}}), ":\n";
    }
    if (length($msg))
    {
      # handle CR, e.g. in git rebase -p output
      $msg =~ s/\r(?!\n)/\n/sg;
      $msg =~ s/^/  /mg;
    }
    print $msg;
  }
}



######################################################################
#
# Check for rebase-in-progress/conflicted-merge, untracked files,
# uncommitted changes (whether staged or not), stashed changes, unpushed
# commits, unmerged tracking branches, or unknown other errors, and die()
# if any are found.  Lack of a tracking branch in a repository is also
# fatal.
#
sub releasecheck($@);
sub releasecheck($@)
{
  my ($cmd, @list) = @_;
  my $errs;
  my %msg;
  my @notonbranch;
  my @untracked;
  my @uncommitted;
  my @stashed;
  my @unpushed;
  my @unmerged;
  my @notracking;
  my @unknown;

  # This works much like `gits statuses` except that it does not assume that
  # the master repository has all the branches of interest, so it processes
  # each repository separately, once looking at the current branch for
  # untracked files, uncommitted changes, and unpushed commits, and a second
  # time looking at other branches for unpushed commits.

  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});

  # suppress warning about missing slaves
  $warngitslavesync = 1;

  my $foundanything;

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    my $unclean;

    next if missing_slave($slave);

    $foundanything++;

    if (! -d "$slave/.git")
    {
      $errs .= "$repo is not a git repository\n";
      next;
    }

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git status ^,$slave);
    if ($ret != 0 && $ret != 256)
    {
      $errs .= "git status failed for: '$repo': " . exitcode($ret) . "\n";
      $errs .= " $msg" if ($msg && $msg !~ /^\s$/);
      last;
    }

    my ($premove);
	die "gits unexpected status output (missing branch): $msg" unless ($msg =~ s/^(?:# )?(?:On branch |Not currently on any branch.)(.+)?\n?//);
    
    my $localbranch = $2;
    if ($1 =~ /^Not/)
    {
      push(@notonbranch,$repo) if ($1);
      $unclean = 1;
      $localbranch = '';
    }

    my $bad;
    if ($msg =~ /^\# Your branch(.*)\n/)
    {
      if ($1 !~ / can be fast-forwarded/)
      {
	$bad = 1;
	push(@unpushed, $repo);
	$unclean = 1;
	$localbranch .= '@';
      }
      while ($msg =~ s/^(\# \S.*\n)//)
      {
	$premove .= $1 if ($bad);
      }
      $msg =~ s/^\#\s*\n//;
    }
    # working directory & stash changes don't need branch qualification
    $localbranch = '' if (!$bad);

    $msg =~ s/^no(thing| changes)( added)? to commit .*\n//m;

    while ($msg =~ s/^(\# \S.*\n)//)
    {
      $unclean = 1;
      $premove .= $1;
      if ($1 =~ /^# Change/)
      {
	push(@uncommitted, $repo) if ($uncommitted[$#uncommitted] ne $repo);
      }
      elsif ($1 =~ /^# Untrack/)
      {
	push(@untracked, $repo) if ($untracked[$#untracked] ne $repo);
      }
      else
      {
	push(@unknown, $repo) if ($unknown[$#unknown] ne $repo);
      }

      while ($msg =~ s/^(\#\s{2,}\S.*\n)//)
      {
	$premove .= $1 if ($1 =~ /discard changes/);
      }
      die "Could not parse git status output for $slave <$msg>\n" unless ($msg =~ s/^(\#\s*\n)//);

      while ($msg =~ s/^(\#(?:\s*|(\s{2,}|\t)\S.*)\n)//)
      {
	my ($line) = $1;

	if ($line =~ /([^:]+:\s+)(.*)/s)
	{
	  $line = "$1$slave/$2";
	}
	elsif ($line =~ /(\#\s+)(.+)/)
	{
	  $line = "$1$slave/$2\n";
	}
	else
	{
	  next if ($line =~ /\#\s*\n/);
	}

	$premove .= $line;
      }
    }

    my $stashes;
    ($ret, $stashes) = getcmd(qq^cd "%%dir%%" && $git stash list^,$slave);
    if ($ret)
    {
      $errs .= "git stash list failed for: '$repo': " . exitcode($ret) . "\n";
      $errs .= " $stashes" if ($stashes && $stashes !~ /^\s$/);
      last;
    }
    elsif ($stashes)
    {
      push(@stashed, $repo);
      $unclean = 1;
      # strip out previous commit from WIP stashes (messages are misleading)
      $stashes =~ s/^(stash\@{\d+}: )WIP( on .*: [\da-f]+)\.\.\..*$/${1}Work-in-Progress${2}/mg;
      $msg .= $stashes;
    }

    if ($unclean)
    {
      push(@{$msg{$premove.$msg}}, "$localbranch$repo");
    }
    else
    {
      push(@{$msg{""}}, "$localbranch$repo");
    }
  }

  $errs .= "You are not on any branch (maybe rebase or unresolved conflicts?) in:\n " .
    join(' ', @notonbranch) . "\n" if (@notonbranch);
  $errs .= "There are uncommitted changes in:\n " .
    join(' ', @uncommitted) . "\n" if (@uncommitted);
  $errs .= "There are unknown other issues in:\n " .
    join(' ', @unknown) . "\n" if (@unknown);

  # Untracked files, stashed changes and unpushed commits are not
  # considered fatal (yet), since we can still safely change to another
  # branch if these are present (unlike not being on a branch, or having
  # uncommitted changes to the working directory).
  my $fatal = $errs;
  $errs .= "There are untracked files in:\n " .
    join(' ', @untracked) . "\n" if (@untracked);
  $errs .= "There are stashed changes in:\n " .
    join(' ', @stashed) . "\n" if (@stashed);

  # Don't add @unpushed yet (we may add repos on other branches)

  if ($fatal)
  {
    stdout(\%msg,1);
    $errs .= "There are unpushed commits in:\n " .
      join(' ', @unpushed) . "\n" if (@unpushed);
    die $errs . "gits $cmd aborted (use --force to override)\n";
  }

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    my ($unclean, $unknown);

    next if missing_slave($slave);

    my $oldbranch =
      (grep { s/\s*\*\s+// }
       split(/\n/,getcmd(qq^cd %%dir%% && $git branch --no-color^, $slave)))[0];
    my @branches = grep { s/branch\.(.*)\.remote=.*/$1/ }
      split(/\n/,getcmd(qq^cd %%dir%% && $git config -l^, $slave));
    my $trackpat = join('|', @branches);
    my %merged;

    push(@notracking, $repo) if ($#branches < 0);
    foreach my $branch (@branches)
    {
      if ($branch eq $oldbranch)
      {
	# original branch is a tracked branch, any branches merged into it
	# are okay to release
	foreach my $merge (grep { s/^  // }
			   split(/\n/,getcmd(qq^cd %%dir%% && $git branch --no-color --contains $oldbranch^, $slave)))
	{
	  $merged{$merge} = 1;
	}
	# we don't need to check for unpushed as we have already done this
	next;
      }

      my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git checkout $branch^,$slave);
      if ($ret)
      {
	$errs .= "git checkout failed for: '$repo': " . exitcode($ret) . "\n";
	$errs .= " $msg" if ($msg && $msg !~ /^\s$/);
	last;
      }

      foreach my $merge (grep { s/^  // }
			 split(/\n/,getcmd(qq^cd %%dir%% && $git branch --no-color --merged^, $slave)))
      {
	$merged{$merge} = 1;
      }

      $msg =~ s/Switched to branch.*\n//;
      $msg =~ s/Already on \".*\n//;

      my $bad;
      my ($premove);
      if ($msg =~ /^Your branch(.*)\n/)
      {
	if ($1 !~ / can be fast-forwarded/)
	{
	  $bad = 1;
	  push(@unpushed, "$branch\@$repo");
	  $unclean = 1;
	}
	while ($msg =~ s/^(.*\S.*\n)//)
	{
	  $premove .= "# " . $1 if ($bad);
	}
	$msg =~ s/^\#?\s*\n//;
      }

      while ($msg =~ s/^(.*\S.*\n)//)
      {
	$unknown = $unclean = 1;
	$premove .= $1;
      }
      $msg =~ s/^\#?\s*\n//;

      push(@unknown, "$branch\@$repo") if ($unknown);

      if ($unclean)
      {
	push(@{$msg{$premove.$msg}}, "$branch\@$repo");
      }
      else
      {
	push(@{$msg{""}}, "$branch\@$repo");
      }
    }

    my @privates;
    foreach my $private (grep { s/^  // && !/^($trackpat)$/ }
			 split(/\n/,getcmd(qq^cd %%dir%% && $git branch --no-color --no-merged^, $slave)))
    {
      push (@privates, $private) if (!$merged{$private});
    }
    $errs .= "Repository $repo has unmerged private branches: "
      . join(' ', @privates) . "\n" if (@privates);

    docmd(qq^cd "$slave" && $git checkout $oldbranch >/dev/null 2>&1^);
  }

  $errs .= "There are unpushed commits in:\n " .
    join(' ', @unpushed) . "\n" if (@unpushed);
  $errs .= "There are unmerged private branches in:\n " .
    join(' ', @unmerged) . "\n" if (@unmerged);
  $errs .= "There are no tracking branches in:\n " .
    join(' ', @notracking) . "\n" if (@notracking);
  $errs .= "There are unknown other issues in:\n " .
    join(' ', @unknown) . "\n" if (@unknown);

  if ($errs)
  {
    stdout(\%msg,1);
    die $errs . "gits $cmd aborted (use --force to override)\n";
  }

  return $foundanything;
}



######################################################################
#
# Main functionality
#
Getopt::Long::Configure("bundling", "no_ignore_case", "no_auto_abbrev", "no_getopt_compat", "require_order");
GetOptions(\%OPTIONS, 'parallel|p=i', 'verbose|v+', 'quiet', 'rawout', 'help', 'version',
	   'pager|paginate!', 'n', 'eval-args', 'exclude=s', 'keep-going',
	   'no-commit', 'no-hide', 'no-progress', 'press-on',
	   'no-master', 'with-ifpresent', 'just-ifpresent') || die $USAGE;
# -n option is separate from --pager/--no-pager/--paginate/--no-paginate due
# to limitations of GetOptions API - the last explicit option overrides -n,
# even if -n comes after the last explicit option.  Note that -n is *also*
# interpreted as --no-commit for the `gits release` subcommand (only).

$OPTIONS{'no-hide'} = 1 if ($OPTIONS{'rawout'});

while ($ARGV[0] =~ /^--/)
{
  $git .= " " . shift @ARGV;
}
# provide baka terminology compatibility
my $BAKANAMES = { 'inits' => 'prepare', 'clones' => 'attach', 'execs' => 'exec',
		  'checkouts' => 'populate', 'depopulate' => 'release',
		  'resolves' => 'resolve', 'stati' => 'statuses' };
$ARGV[0] = $BAKANAMES->{$ARGV[0]} if $BAKANAMES->{$ARGV[0]};
$OPTIONS{'keep-going'} = 1 if $OPTIONS{'press-on'};

$want_parallel = $OPTIONS{'parallel'} if (defined($want_parallel));
$OPTIONS{'no-progress'} = 1 if ($want_parallel);

$OPTIONS{'no-master'} = 1 if ($OPTIONS{'just-ifpresent'});

# default is pagination; however, if verbose is set, default is no pager
# because verbose generates warnings to STDERR, which is not paginated
$pagination = !$OPTIONS{'verbose'};
# unless gits -n
$pagination = 0 if ($OPTIONS{'n'});
# but explicit --pager/--no-pager/--pagination/--no-pagination overrides that
$pagination = $OPTIONS{'pager'} if (defined $OPTIONS{'pager'});

die "$USAGE" if ($#ARGV < 0 and !($OPTIONS{'version'} or $OPTIONS{'help'}));

print STDERR $eval_errs if ($eval_errs ne '' && $OPTIONS{'verbose'} > 1);

# Find GITS_DIR -- location of git slave file
if ($ENV{'GITS_DIR'} && -f $ENV{'GITS_DIR'}."/".GITSLAVE)
{				# Use gitslave location user told us
  $GITS_DIR = $ENV{'GITS_DIR'};
}
else
{				# Probe for gitslave location
  $GITS_DIR = '.';
  my (@S,$dev,$ino) = stat($GITS_DIR);

  do
  {
    if (!-f $GITS_DIR."/".GITSLAVE)
    {
      $GITS_DIR .= '/..';
      $dev = $S[0];
      $ino = $S[1];
      @S = stat($GITS_DIR);
    }
  } while (!-f $GITS_DIR."/".GITSLAVE && $S[0] == $dev && $S[1] != $ino);
}

# All (non-prepare/clone) operations are relative to gitslave base
if (-f $GITS_DIR."/".GITSLAVE)
{
  $GITS_DIR = Cwd::realpath($GITS_DIR);
  chdir($GITS_DIR) unless ($ARGV[0] eq "prepare" || $ARGV[0] eq "clone");
  print STDERR "gits: Gitslave found in $GITS_DIR\n" if ($OPTIONS{'verbose'} > 1);
}
else
{
  die "Could not find @{[GITSLAVE]}, either you are not inside a superproject\nor one of its slaves or this is a new superproject that has not yet\nbeen configured for gits.  Probably it is the first case so you simply\nneed to cd into the correct git checkout with a @{[GITSLAVE]} file, but in\nthe rare event that this is a new superproject, you need run `gits\nprepare` and then a few `gits attach` commands to set up gits for this\nnew superproject.\n"
    unless ($#ARGV < 0 or ephemeral_command($ARGV[0]));
}
$TOP = "(" . basename(Cwd::realpath(".")) . ")";

# Get configured pager from environment and/or master (superproject) settings
# and redirect standard output through pager if it is currently a terminal
# (and a controlling tty is available from which the pager can take commands)
if ($pagination && -t STDOUT && open(TTY, "</dev/tty"))
{
  close(TTY);
  my $PAGER = `$git config core.pager`;
  chomp $PAGER;
  # PAGER environment variable or default only used if core.pager not set
  $PAGER = $ENV{'PAGER'} || "less" if ($?);
  # GIT_PAGER environment variable trumps everything else
  $PAGER = $ENV{'GIT_PAGER'} if (defined $ENV{'GIT_PAGER'});

  # Windows less doesn't seem to have -K
  if ($^O ne "MSWin32" && $^O ne "cygwin" && $^O ne "msys")
  {
    # for less, explicitly add -K so that SIGINT will terminate (and cleanup)
    $PAGER .= " -K" if ($PAGER =~ m!^([^\s\(\`;]*/)?less([-+\$\w ]*)?$!);
  }

  if ($PAGER && $PAGER ne "cat")
  {
    my $lessenv = $ENV{'LESS'} || "FRSX";

    # gits is pretty unusable without -F, and -F requires -X
    $lessenv =~ s/^(-?)/${1}X/ if $lessenv !~ /X/;
    $lessenv =~ s/^(-?)/${1}F/ if $lessenv !~ /F/;

    # set LESS environment for pager only (not later git subcommands)
    $PAGER = "LESS='$lessenv' " . $PAGER ;

    my $pid;
    if ($pid = open(STDOUT, "|$PAGER"))
    {
      # cleanup pager on die
      $SIG{__DIE__} = sub
	{
	  die @_ if $^S; # do nothing if die called within eval
	  kill 'INT', $pid;
	  close STDOUT;
	};
    }
    else
    {
      warn "Unable to run pager '$PAGER': $!\n" unless ($OPTIONS{'quiet'});
    }
  }
}
@slaves = git_slave_list($ARGV[0]);

if (!$OPTIONS{'no-progress'})
{
  if (defined($want_progress) && -t STDERR)
  {
    $want_progress = 1;
  }
  else
  {
    warn "Progress bar unavailable - install Term::ProgressBar\n" .
      " (perl-Term-ProgressBar RPM) (libterm-progressbar-perl dpkg)\n"
      if ($OPTIONS{'verbose'});
  }
}

if ($OPTIONS{'parallel'})
{
  if (!defined($want_parallel))
  {
    warn "Parallelism unavailable - install Parallel::Iterator\n" .
      " (no RPM or dpkg exists, use CPAN)\n";
  }
}


##################################################
if ($ARGV[0] eq 'version' or $OPTIONS{'version'})
{
  my $version = "2.0.2";

  # <TRICKY> If the version string is still UNTAGGED, try to run the local
  # git commands to get the current hash and possibly tag; this is used by
  # Makefile to get the correct string to replace it with.  Otherwise, the
  # installation has replaced it with a version; don't mess with it.  Use
  # string concatenation to keep this check from getting replaced. </TRICKY>

  if ($version eq "{" . "UNTAGGED" . "}")
  {
    my ($ret,$hash) = getcmd("$git log --pretty=format:%H -n 1");
    chomp($hash);

    my ($dret,$desc) = getcmd("$git describe --candidates=1 --tags $hash 2>/dev/null");
    if ($dret)
    {
      $desc = "Untagged ($hash)\n";
    }
    else
    {
      # strip out leading tag alphabetic and space, get just the numbers
      $desc =~ s/^.*?([0-9][0-9._-]*[0-9]).*/$1/;
      $desc =~ y=/_=--=;
    }
    print $desc;
  }
  else
  {
    print "gits version $version\n";
    my ($vret,$vers) = getcmd("$git --version");
    if ($vret || ($vers !~ /version ([2-9]|1\.[6-9])/))
    {
      print STDERR "$git version >= 1.6 required!\n";
      $returncode = 1;
    }
    print $vers;
    ($vret,$vers) = getcmd("perl --version");
    $vers =~ s/^\n//;
    $vers =~ s/This is perl,/Perl/;
    $vers =~ s/\nCopyright.*//s;
    print $vers;
  }
}
##################################################
elsif ($ARGV[0] eq 'help' or $OPTIONS{'help'})
{
  print "$USAGE";
  use FindBin qw($Bin $Script);
  docmd("pod2text < $Bin/$Script |" .
	"sed -n -e '/^DESCRIPTION/,/^BUGS/H'" .
	" -e '/^BUGS/{x;s/\\n[[:upper:]]*/\\n/g;s/\\n\\n/\\n/;p;}'");
  exit;
}
##################################################
elsif ($ARGV[0] eq 'prepare')
{
  die "gits prepare takes no arguments\n" if ($#ARGV != 0);
  my $SUPER_GITS_DIR = $GITS_DIR;
  $GITS_DIR=$ENV{'GITS_DIR'} || ".";
  die "Refusing to prepare, \$GITSLAVE variable is multipath\n" if ($GITSLAVE =~ /, /);
  die "Refusing to prepare, $GITS_DIR/@{[$GITSLAVE]} already exists\n" if (-f $GITS_DIR."/".$GITSLAVE);
  die "Refusing to prepare, $SUPER_GITS_DIR/@{[$GITSLAVE]} already exists\n"
    . " (GITS_DIR=".`pwd`." gits prepare will override)\n" if (-f $SUPER_GITS_DIR."/".$GITSLAVE && !defined($ENV{'GITS_DIR'}));
  open(W, ">", $GITS_DIR."/".$GITSLAVE) || die "Could not create $GITS_DIR/@{[$GITSLAVE]}\n";
  close(W);
  docmd("$git add @{[$GITSLAVE]}");
  docmd(qq^$git commit -m "gits creating @{[$GITSLAVE]}" @{[$GITSLAVE]}^) unless ($OPTIONS{'no-commit'});
}
##################################################
elsif ($ARGV[0] eq 'attach')
{
  my $include;
  my $reference = "";
  my $adminonly;

  if ($include = (grep(/^--recursive=(.+)$/,@ARGV))[0])
  {
    $include =~ s/--recursive=//;
    die "--recursive argument may not have directory components: it must be in base of new slave\n" if ($include =~ m:/:);
    @ARGV = grep(!/^--recursive=.+$/,@ARGV);
  }

  # Try to save bandwidth
  if ($reference = (grep(/^--reference=.*$/,@ARGV))[0])
  {
    $reference = quoteit($reference);
    @ARGV = grep(!/^--reference=.*/,@ARGV);
  }

  # Help people not wanting to reclone or use --reference
  if ($adminonly = (grep(/^--adminonly$/,@ARGV))[0])
  {
    $adminonly = 1;
    @ARGV = grep(!/^--adminonly/,@ARGV);
  }

  die "gits attach requires at least two non-option arguments\nUsage: gits attach [--reference=PATH] [--recursive=FILENAME] [--adminonly] REPOSITORY LOCALPATH [FLAGS]\n" if ($#ARGV < 2 || !$ARGV[1] || !$ARGV[2]);
  my ($repos) = resolve_repository($ARGV[1],$ARGV[2]);
  die "Unknown flag $ARGV[3] (only flag is 'ifpresent')\n"
    if ($ARGV[3] && $ARGV[3] ne "ifpresent");
  die "Too many flags " . join(' ',@ARGV[3..$#ARGV]) . "\n" if ($#ARGV > 3);
  die "GITSLAVE environment variable specifies multiple files - cannot attach\n"
    if ($GITSLAVE =~ /, /);
  die "Destination ($ARGV[2]) already exists\n" if (-e $ARGV[2] && !$adminonly);
  if ($ARGV[2] =~ m:(.*)/(.*):)
  {
    die "Destination ($ARGV[2]) cannot end in a slash" if (length($2) < 1);
    die "Destination parent ($1) does not exist\n" if (!-d $1);
  }
  my $files = $GITS_DIR."/".$GITSLAVE;
  if (!$adminonly)
  {
    docmd(qq^$git clone $reference @{[quoteit($repos)]} @{[quoteit($ARGV[2])]}^) || die "Could not clone $repos onto $ARGV[2]\n";
  }

  open(W, ">>", $files);
  print W qq^"$ARGV[1]" "$ARGV[2]"^;
  print W " $ARGV[3]" if ($ARGV[3]);
  print W "\n";
  if ($include)
  {
    print W qq^#include "$ARGV[2]/$include"^;
    print W " $ARGV[3]" if ($ARGV[3]);
    print W "\n";
  }
  close(W);

  if (1)
  {
    my ($gitignore, $ignorefile) = findignore($ARGV[2]);

    my ($needadd) = 0;
    $needadd++ if (! -f "$gitignore/.gitignore");
    open(W, ">>", $gitignore."/.gitignore");
    print W qq^/$ignorefile/\n^;
    close(W);
    docmd(qq^cd "$gitignore" && $git add .gitignore^) if ($needadd);
    if ($gitignore eq $GITS_DIR)
    {
      $files .= " .gitignore";
    }
    else
    {
      my $msg = qq^gits adding "$ARGV[1]" "$ARGV[2]" (.gitignore)^;
      docmd(qq^cd "$gitignore" && $git commit -m '$msg' .gitignore^) unless ($OPTIONS{'no-commit'});
    }
  }
  my $msg = qq^gits adding "$ARGV[1]" "$ARGV[2]"^;
  $msg .= qq^ and recursively $include^ if ($include);
  docmd(qq^$git commit -m '$msg' $files^) unless ($OPTIONS{'no-commit'});

  if (!$adminonly)
  {
    my $curbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd("$git branch --no-color"))))[0];
    my $subbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd(qq^cd "$ARGV[2]" && $git branch --no-color^))))[0];
    if ($curbranch ne $subbranch)
    {
      print qq^Switching "$ARGV[2]" to branch "$curbranch"\n^;
      docmd(qq^cd "$ARGV[2]" && $git checkout $curbranch --^) || die "Branch inconsistency, branch $curbranch does not exist\n";
    }
  }
}
##################################################
elsif ($ARGV[0] eq 'detach')
{
  my $force;

  if (grep(/^--force$/,@ARGV))
  {
    $force = 1;
    @ARGV = grep(!/^--force$/,@ARGV);
  }

  die "gits detach requires one (slave) directory argument, which must exist\nUsage: gits detach [--force] SLAVE\n" if ($#ARGV != 1);

  die "GITSLAVE environment variable specifies multiple files - cannot detach\n"
    if ($GITSLAVE =~ /, /);

  my $slaveref = (grep($_->[1] eq $ARGV[1], @slaves))[0];

  die "$ARGV[1] is not an attached git slave\n" unless ($slaveref);

  if (-d "$ARGV[1]")
  {
    my $gitdir1 = `cd "$ARGV[1]/.."; x=\$($git rev-parse --git-dir); if [ -d "\$x" ]; then cd \$x; pwd; fi`;
    my $gitdir2 = `cd "$ARGV[1]"; x=\$($git rev-parse --git-dir); if [ -d "\$x" ]; then cd \$x; pwd; fi`;
    die "$ARGV[1] exists but is not a checked out git directory\n" unless ($gitdir1 ne $gitdir2);
  }

  # <TODO>check whether it found slave directory at all?</TODO>
  $OPTIONS{'no-master'} = 1;
  releasecheck($ARGV[0], ($slaveref)) unless ($force);

  my $files = $GITS_DIR."/".$GITSLAVE;
  die "The gitslave file '$files', does not exist\n" unless (-f $files);
  open(R,"<",$files) || die "Cannot open $files";
  my $save;
  my ($lineno) = 0;
  my $found = 0;
  while (<R>)
  {
    s/\r$//g;
    $lineno++;
    if (/^\s*(?:\#.*)$/)
    {
      $save .= $_;
      next;
    }
    die "Bad line $lineno in $files\n" unless (/^\"(.*)\" \"(.*)\"( ifpresent)?$/);
    if ($2 eq $ARGV[1])
    {
      $found++;
      next;
    }
    $save .= $_;
  }
  close(R);
  die "Could not find requested repository $ARGV[1] in $files for detaching.  Aborting\n" unless ($found);
  open(W,">",$files) || die "Cannot open $files for writing";
  print W $save;
  close(W) || die "Cannot write $files";

  # remove slave repo before we take it out of .gitignore, to avoid "untracked"
  docmd(qq^rm -rf ^.quoteit($ARGV[1]))
    unless ($OPTIONS{'no-commit'} || $OPTIONS{'force'});

  my ($gitignore, $ignorefile) = findignore($ARGV[1]);

  if (open(R,"<","$gitignore/.gitignore"))
  {
    my $save2;
    while (<R>)
    {
      s/\r$//g;
      my ($c) = $_;
      chomp($c);
      next if ($c eq "/$ignorefile/");
      $save2 .= $_;
    }
    close(R);
    open(W,">","$gitignore/.gitignore") || die "Cannot open .gitignore for writing";
    print W $save2;
    close(W) || die "Cannot write .gitignore";
    docmd(qq^cd "$gitignore" && $git commit -m 'gits removing ^.quoteit($ARGV[1])."' .gitignore") unless ($gitignore eq $GITS_DIR || $OPTIONS{'no-commit'});
  }

  docmd(qq^$git commit -m 'gits removing ^.quoteit($ARGV[1])."' ".quoteit($GITSLAVE).($gitignore eq $GITS_DIR?" .gitignore":"")) unless ($OPTIONS{'no-commit'});
}
##################################################
elsif ($ARGV[0] eq 'clone')
{
  my $nohooks;
  my $fromcheckout;
  my $reference;
  if (grep(/^--nohooks$/,@ARGV))
  {
    $nohooks=1;
    @ARGV = grep(!/^--nohooks$/,@ARGV);
  }

  if (grep(/^--fromcheckout$/,@ARGV))
  {
    $fromcheckout=1;
    @ARGV = grep(!/^--fromcheckout$/,@ARGV);
  }

  if (grep(/^--no-fromcheckout$/,@ARGV))
  {
    $fromcheckout=0;
    @ARGV = grep(!/^--no-fromcheckout$/,@ARGV);
  }

  $reference=(grep(/^--reference(checkout)?=(.*)$/,@ARGV))[0];
  my (@cARGV) = @ARGV;
  grep(s/--referencecheckout=/--reference=/,@cARGV);

  my ($ret,$out) = getcmd(qq^$git ^.quoteit(@cARGV));
  print STDERR $out unless ($OPTIONS{'quiet'});
  if ($ret != 0 ||
      ($out !~ /^Cloning into '?(.*?)'?\.\.\.\n/
       && $out !~ m^Initialized empty Git repository in (.*)/\.git/\n^))
  {
    die "Could not parse git clone output\n";
  }
  $GITS_DIR = Cwd::realpath($1);
  chdir($1);

  docmd(qq^$git config gits.nohooks 1^) if ($nohooks);
  set_fromcheckout($fromcheckout,"origin") if (defined($fromcheckout));

  my $cmd = "$0 ";
  foreach my $arg (keys %OPTIONS)
  {
    if ($arg eq "exclude" || $arg eq "parallel")
    {
      $cmd .= quoteit("--$arg=".$OPTIONS{"$arg"})." ";
    }
    else
    {
      my $cnt = $OPTIONS{$arg};
      while ($cnt--)
      {
	$cmd .= "--$arg ";
      }
    }
  }
  $cmd .= "populate";
  if ($reference)
  {
    $cmd .= " ".quoteit($reference);
  }
  docmd($cmd);
}
##################################################
elsif ($ARGV[0] eq 'populate')
{
  my ($reference);

  if (grep(/^--nohooks$/,@ARGV))
  {
    docmd(qq^$git config gits.nohooks 1^);
    @ARGV = grep(!/^--nohooks$/,@ARGV);
  }

  if (grep(/^--fromcheckout$/,@ARGV))
  {
    set_fromcheckout(1,"origin");
    @ARGV = grep(!/^--fromcheckout$/,@ARGV);
  }

  if (grep(/^--no-fromcheckout$/,@ARGV))
  {
    set_fromcheckout(0,"origin");
    @ARGV = grep(!/^--no-fromcheckout$/,@ARGV);
  }

  # accept git option --with-ifpresent here too
  if (grep(/^--with-ifpresent$/,@ARGV))
  {
    @ARGV = grep(!/^--with-ifpresent$/,@ARGV);
    if (!$OPTIONS{'with-ifpresent'})
    {
      $OPTIONS{'with-ifpresent'} = 1;
      @slaves = git_slave_list($ARGV[0]) if (!$#ARGV);
    }
  }

  # Try to save bandwidth
  if ($reference = (grep(/^--reference(checkout)?=.*$/,@ARGV))[0])
  {
    $reference =~ s/--reference(checkout)?=//;
    $reference = [$reference,$1];
    @ARGV = grep(!/^--reference(checkout)?=.*/,@ARGV);
  }

  if ($#ARGV > 0)
  {
    my $cmd = shift(@ARGV);
    @slaves = git_slave_list($cmd,@ARGV);
  }

  do_populate(undef,$reference);
}
##################################################
elsif ($ARGV[0] eq 'release')
{
  my ($force,$all);
  my @list = @slaves;

  shift @ARGV;

  if (grep(/^--force$/,@ARGV))
  {
    $force = 1;
    @ARGV = grep(!/^--force$/,@ARGV);
  }

  if (grep(/^--all$/,@ARGV))
  {
    $all = 1;
    @ARGV = grep(!/^--all$/,@ARGV);
  }

  # accept git options --just-ifpresent, --no-master, and --no-commit here too
  if (grep(/^--just-ifpresent$/,@ARGV))
  {
    @ARGV = grep(!/^--just-ifpresent$/,@ARGV);
    if (!$OPTIONS{'just-ifpresent'})
    {
      $OPTIONS{'just-ifpresent'} = $OPTIONS{'no-master'} = 1;
      @list = git_slave_list("release");
    }
  }
  if (grep(/^--no-master$/,@ARGV))
  {
    @ARGV = grep(!/^--no-master$/,@ARGV);
    if (!$OPTIONS{'no-master'})
    {
      $OPTIONS{'no-master'} = 1;
      @list = git_slave_list("release");
    }
  }
  if (grep(/^(-n|--no-commit)$/,@ARGV))
  {
    @ARGV = grep(!/^(-n|--no-commit)$/,@ARGV);
    $OPTIONS{'no-commit'} = 1;
  }

  # gits option -n (meaning --no-pager) is *also* --no-commit for release
  $OPTIONS{'no-commit'} = 1 if ($OPTIONS{'n'});

  my $usage = "\nUsage: gits release [-n|--no-commit] [--force] [--no-master]\n\t--all | --just-ifpresent | SUPERPROJECT | SLAVES..\n";

  die "Cannot combine --just-ifpresent or --all with slave args" . $usage
    if (1 < $all + $OPTIONS{'just-ifpresent'}
	    + (@ARGV && $ARGV[0] !~ m=^/=));

  die "Must give --just-ifpresent, --all, superproject, or slave args" . $usage
    if (1 > $all + $OPTIONS{'just-ifpresent'} + @ARGV);

  die "For safety --force and --all cannot be used together" . $usage
    if ($force && $all);

  if (@ARGV)
  {
    if (grep(m=^/=, @ARGV))
    {
      die "Absolute path must be only argument to gits release" . $usage
	if (@ARGV > 1);
      die "Absolute path cannot be used with --force" . $usage
	if ($force);
      $GITS_DIR = $ARGV[0];
      die "$GITS_DIR: $!" . $usage unless chdir($GITS_DIR);
      die "Not a gitslave repository: $GITS_DIR" . $usage
	unless (-f GITSLAVE && -d ".git");
      @list = git_slave_list("release");
      $all = 1;
    }
    else
    {
      # just be a little bit extra paranoid about args, since we will rm -rf 'em
      my @bad = grep(/^\.|[\r\n;'"`]/, @ARGV);
      die "Illegal slave name: " . quoteit(@bad) . $usage if (@bad);
      @list = ();
      $OPTIONS{'no-master'} = 1;
      foreach my $slave (@ARGV)
      {
	die "Not a git repository: $slave" . $usage
	  unless (-d "$slave/.git");
	die "Superproject $slave must be released as absolute path\n ( " .
	  $GITS_DIR . "/" . $slave . " )" . $usage
	  if (-f "$slave/" . GITSLAVE);
	push (@list, ['', $slave]);
      }
    }
  }

  die "Could not find @{[GITSLAVE]} or git directory in " . Cwd::realpath(".") .
    "\n To release entire superproject: `gits release /path/to/superproject`" .
      "\n Otherwise you must be inside a superproject or one of its slaves\n"
	unless (-f GITSLAVE && -d ".git");

  if (@list == 0 && $OPTIONS{'no-master'})
  {
    warn "Nothing to do\n" unless ($OPTIONS{'quiet'});
  }
  # skip releasecheck if --force, nothing to do if it returns undef
  elsif (!$force && !releasecheck("release", @list))
  {
    warn "Already removed" . join('', map(" " . $_->[1], @list)) . "\n"
      unless ($OPTIONS{'quiet'});
  }
  else
  {
    @list = map($_->[1], @list);

    # more paranoia - don't remove / or slaves with absolute or dot path or /../
    $OPTIONS{'no-commit'} = 1
      if ($GITS_DIR eq "/" || grep(m=^[/.]|/\.\./|/\.\.$=, @list));

    my ($echo, $not);
    if ($OPTIONS{'no-commit'})
    {
      $echo = "echo '\#' ";
      $not = "Not ";
    }

    # a single . indicates that master is being removed
    if (($all && !$OPTIONS{'no-master'}) || grep(/^.$/,@list))
    {
      print $not . "removing $GITS_DIR (and slaves)\n";
      docmd($echo . qq^rm -rf -- ^ . quoteit($GITS_DIR));
    }
    else
    {
      print $not . "removing " . join(' ',@list) ."\n";
      docmd($echo . qq^rm -rf -- ^ . quoteit(@list));
    }
  }
}
##################################################
elsif ($ARGV[0] eq 'resolve')
{
  if (grep(/^--fromcheckout$/,@ARGV))
  {
    $fromcheckout = 1;
    @ARGV = grep(!/^--fromcheckout$/,@ARGV);
  }

  if (grep(/^--no-fromcheckout$/,@ARGV))
  {
    $fromcheckout = 0;
    @ARGV = grep(!/^--no-fromcheckout$/,@ARGV);
  }

  die "gits resolve requires at least two non-option arguments <possibly-relative url> <location-in-repository> [remote]\n" if ($#ARGV < 2 || $#ARGV > 3 || !$ARGV[1] || !$ARGV[2]);

  print resolve_repository($ARGV[1], $ARGV[2], $ARGV[3])."\n";
}
##################################################
elsif ($ARGV[0] eq 'pulls')
{
  shift(@ARGV);
  my ($ok,$okcnt,%msg);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my $oldbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd("$git branch --no-color"))))[0];
  my @branches = grep(s/branch\.(.*)\.remote=.*/$1/,split(/\n/,getcmd(qq%$git config -l%)));
  my $fetched;
  my $rebase;
  my $force = 0;
  my @fetchargs = grep(/^(-[qvaftku]+|--(no-)?(quiet|verbose|append|force|tags|keep|update-head-ok|depth=.*|upload-pack=.*))$/, @ARGV);
  my @mergeargs = grep(/^(-[n]|--(no-)?(stat|summary|log|commit|squash|ff|strategy=.*))$/, @ARGV);
  my $rebasearg = " -p";

  # sort branches to list current branch of master repository first
  my $curbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd("$git branch --no-color"))))[0];
  @branches = sort { ($b eq $curbranch) <=> ($a eq $curbranch) } @branches;

  # is this a rebase or merge (default) pull?
  foreach my $arg (@ARGV)
  {
    $rebase = 1 if $arg eq "--rebase";
    $rebase = 0 if $arg eq "--no-rebase";
    # bundled single-character options (e.g. -abc) and non-option arguments
    # (args to options, e.g. "-s ours", or repository/refspec) are too tricky;
    # the special end-of-options flag (--) falls in that category too
    $fetched = -1 if ($arg !~ /^-/ || $arg eq "--" || $arg =~ /^-[^-]{2}/);
  }
  $rebase = -1 if ($rebase && $fetched < 0);
  # only git version 1.6.1 and later support `git rebase -p`
  my $gitversion = `$git --version`;
  if ($rebase < 0 ||
      ($rebase > 0 && $gitversion =~ /git version (1\.[12345]\.|1\.6\.0)/))
  {
    $rebasearg = "";
    warn "Warning: merges may be lost when rebasing!\n" unless ($OPTIONS{'quiet'});
    # <TODO>add some user interaction to confirm possible damaging activity?
    # or determine if there are any merge commits that would be lost?</TODO>
  }

  if ($want_progress)
  {
    # steps are checkouts and fetches; if we pull in every repository for every
    # branch, (1 + branches + (repos * branches)), else (1 + branches + repos)
    my $steps;
    if ($fetched < 0)
    {
      $steps = 1 + ($#branches + 1) * ($#list + 2);
    }
    else
    {
      $steps = 1 + ($#branches + 1) + ($#list + 1);
    }

    $progress = Term::ProgressBar->new({remove=>1, ETA => 'linear', count=>$steps});
  }
  $progress->max_update_rate(1) if ($progress);

  foreach my $branch (@branches)
  {
    my (%newmsg) = do_checkout(1, $oldbranch, $branch);
    my (%results);
    my $pullcmd;
    my $getremote = "REM=`$git config branch.$branch.remote`; " .
      "REF=`$git config branch.$branch.merge | sed 's%.*/%%'`; ";

    # default command on first merge or if there are "tricky" arguments
    if ($fetched < 1 && $rebase < 1)
    {
      $pullcmd = "$git pull ".quoteit(@ARGV);
      # check for branch.<name>.rebase = true only on non-tricky first merge
      # (and when rebase has not been explicitly disabled with --no-rebase)
      $pullcmd = $getremote .
	"if [ \"`$git config --bool branch.$branch.rebase`\" = true ]; then " .
	"$git fetch " . quoteit(@fetchargs) . " && " .
	"$git rebase$rebasearg remotes/\"\$REM/\$REF\"; else " .
	"$pullcmd; fi" if (!$fetched && !defined($rebase));
    }
    # subsequent explicit merge (with --no-rebase)
    elsif (defined($rebase) && !$rebase)
    {
      $pullcmd = $getremote .
	"RURL=`$git config remote.\"\$REM\".url`; " .
	"$git merge -m \"Merge branch '\$REF' of \$RURL into $branch\" " .
	quoteit(@mergeargs) . " remotes/\"\$REM/\$REF\"";
    }
    # subsequent merge (or rebase if branch.<name>.rebase is set)
    elsif (!$rebase)
    {
      $pullcmd = $getremote .
	"if [ \"`$git config --bool branch.$branch.rebase`\" = true ]; then " .
	"$git rebase$rebasearg remotes/\"\$REM/\$REF\"; else " .
	"RURL=`$git config remote.\"\$REM\".url`; " .
	"$git merge -m \"Merge branch '\$REF' of \$RURL into $branch\" " .
	quoteit(@mergeargs) . " remotes/\"\$REM/\$REF\"; fi";
    }
    else
    {
      # rebase (with -p if supported by git version)
      $pullcmd = $getremote . "$git rebase$rebasearg remotes/\"\$REM/\$REF\"";
      # preceded by fetch on first time through
      $pullcmd = "$git fetch ".quoteit(@fetchargs)." && { $pullcmd; }"
	if (!$fetched);
    }
    # for debugging
    # $pullcmd = "set -x; $pullcmd";

    foreach my $group (@list)
    {
      my $slave = $group->[1];
      next if missing_slave($slave);

      $results{$group} = gitsubst(qq^cd "%%dir%%" && $pullcmd^,$slave);
    }
    if ($want_parallel)
    {
      %results = Parallel::Iterator::iterate_as_hash({'workers' => $want_parallel},sub { [getcmd($_[1])] }, \%results);
    }

    foreach my $group (@list)
    {
      my $slave = $group->[1];
      my $repo = $slave;
      $repo = $TOP if ($repo eq ".");

      next if missing_slave($slave);

      my ($ret, $msg);
      if ($want_parallel)
      {
	($ret, $msg) = @{$results{$group}};
      }
      else
      {
	($ret, $msg) = getcmd($results{$group});
      }
      if ($ret != 0)
      {
	chomp($msg);
	my $warn = "gits pulls, failed on branch $branch for: '$repo': " . exitcode($ret);
	if ($OPTIONS{'keep-going'} || $want_parallel)
	{
	  $warn = "Error in $warn (continuing)\n $msg\n";
	  $warn = "\n$warn" if ($progress);
	  warn $warn;
	  $returncode = 2;
	  next;
	}

	my $diemsg = "Aborting $warn\n ";
	if ($ok)
	{
	  $diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
	}
	else
	{
	  $diemsg .= "(first entry, no other successfully executed)";
	}
	# <TODO>some way for do_checkout to only operate on some slaves</TODO>
	$msg .= do_checkout(2, $oldbranch);
	$diemsg = "\n$diemsg" if ($progress);
	die "$diemsg\n  $msg\n";
      }
      $msg = $newmsg{$repo}.$msg;
      $msg =~ s/Switched to branch.*\n//;
      $msg =~ s/Already on \".*\n//;
      $msg =~ s/Current branch $branch is( up to date)/Already$1/;
      $msg =~ s+(Successfully rebased and updated) refs/heads/$branch+$1+;
      $ok .= " $repo";
      $okcnt++;
      # replace repository name with %REPO% to collapse redundant messages
      my $mod = $slave;
      if ($mod eq '.')
      {
	$mod = `$git config remote.origin.url`;
	chomp $mod;
	$mod =~ s=.*/==;
      }
      $msg =~ s/\b$mod\b/%REPO%/g;
      my $submod = $mod;
      $submod =~ s=.*/==;
      $msg =~ s/\b$submod\b/%REPO%/g if ($mod ne $submod);
      # remove hash ids in various places to collapse redundant messages
      if (!$OPTIONS{'no-hide'})
      {
	$msg =~ s/\n {3}[0-9a-fA-F]{7}\.\.[0-9a-fA-F]{7} /\n /g;
	$msg =~ s/\nUpdating [0-9a-fA-F]{7}\.\.[0-9a-fA-F]{7}\n/\n/g;
	$msg =~ s/(Fast-forwarded \S+) to [0-9a-fA-F]*\./$1/g;
      }
      push(@{$msg{$msg}}, "$branch\@$repo");

      # fetch / pull is a step
      $progress->update(++$progress_count) if ($progress && $fetched < 1);
    }

    # only fetch for first branch
    $fetched = 1 if (!$fetched);

    # branch checkout is a step
    $progress->update(++$progress_count) if ($progress);
  }

  my $lastmsg = do_checkout(2, $oldbranch);
  $progress->update(++$progress_count) if ($progress);

  stdout(\%msg);

  $lastmsg .= "\n" if ($lastmsg);
  print $lastmsg;
}
##################################################
elsif ($ARGV[0] eq 'pull' || $ARGV[0] eq 'fetch')
{
  my ($ok,$okcnt,%msg);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});

  $progress = Term::ProgressBar->new({remove=>1, ETA => 'linear', count=>($#list+1)}) if ($want_progress);
  $progress->max_update_rate(1) if ($progress);

  my (%results,%worklist);
  foreach my $group (@list)
  {
    my $slave = $group->[1];

    next if missing_slave($slave);

    $worklist{$group} = gitsubst(qq^cd "%%dir%%" && $git ^.quoteit(@ARGV),$slave);
  }

  if ($want_parallel)
  {
    %results = Parallel::Iterator::iterate_as_hash({'workers' => $want_parallel},sub { [getcmd($_[1])] }, \%worklist);
  }

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg);
    if ($want_parallel || ref($results{$group}))
    {
      ($ret, $msg) = @{$results{$group}};
    }
    else
    {
      ($ret, $msg) = getcmd($worklist{$group});
    }
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);
      if ($OPTIONS{'keep-going'} || $want_parallel)
      {
	$warn = "Error in $warn (continuing)\n $msg\n";
	$warn = "\n$warn" if ($progress);
	warn $warn;
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      $diemsg = "\n$diemsg" if ($progress);
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;
    # replace repository name with %REPO% to collapse redundant messages
    my $mod = $slave;
    if ($mod eq '.')
    {
      $mod = `$git config remote.origin.url`;
      chomp $mod;
      $mod =~ s=.*/==;
    }
    $msg =~ s/\b$mod\b/%REPO%/g;
    my $submod = $mod;
    $submod =~ s=.*/==;
    $msg =~ s/\b$submod\b/%REPO%/g if ($mod ne $submod);
    # remove hash ids in various places to collapse redundant messages
    if (!$OPTIONS{'no-hide'})
    {
      $msg =~ s/\n {3}[0-9a-fA-F]{7}\.\.[0-9a-fA-F]{7} /\n /g;
      $msg =~ s/\nUpdating [0-9a-fA-F]{7}\.\.[0-9a-fA-F]{7}\n/\n/g;
      $msg =~ s/(Fast-forwarded \S+) to [0-9a-fA-F]*\./$1/g;
    }
    push(@{$msg{$msg}}, $repo);

    $progress->update(++$progress_count) if ($progress);
  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'push')
{
  my ($ok,$okcnt,%msg);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($quick) = 0;

  if (!$>)
  {
    # git uses $HOME only for configuration search, not ~user
    my $dir = $ENV{'HOME'};
    my @statinfo = stat("$dir/.gitconfig") if ($dir);

    # If ~/.gitconfig exists and is owned by root, root push is allowed
    die "push should not be performed as root\n" unless (exists($statinfo[4]) && $statinfo[4] == $>);
  }

  if (grep(/^--quick$/,@ARGV))
  {
    $quick = 1;
    @ARGV = grep(!/^--quick$/,@ARGV);
  }

  $progress = Term::ProgressBar->new({remove=>1, ETA => 'linear', count=>($#list+1)}) if ($want_progress);
  $progress->max_update_rate(1) if ($progress);

  my (%results,%worklist);
  foreach my $group (@list)
  {
    my $slave = $group->[1];

    next if missing_slave($slave);

    if ($quick)
    {
      my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git status^, $slave);
      if (!($msg =~ /Your branch is /))
      {
	$results{$group} = [0, "Skipping push, this branch is up to date.\n"];
	next;
      }
    }

    $worklist{$group} = gitsubst(qq^cd "%%dir%%" && $git ^.quoteit(@ARGV),$slave);
  }

  if ($want_parallel)
  {
    %results = Parallel::Iterator::iterate_as_hash({'workers' => $want_parallel},sub { [getcmd($_[1])] }, \%worklist);
  }

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg);
    if ($want_parallel || ref($results{$group}))
    {
      ($ret, $msg) = @{$results{$group}};
    }
    else
    {
      ($ret, $msg) = getcmd($worklist{$group});
    }
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);
      if ($OPTIONS{'keep-going'} || $want_parallel)
      {
	$warn = "Error in $warn (continuing)\n $msg\n";
	$warn = "\n$warn" if ($progress);
	warn $warn;
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      $diemsg = "\n$diemsg" if ($progress);
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;
    push(@{$msg{$msg}}, $repo);

    $progress->update(++$progress_count) if ($progress);
  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'checkout')
{
  shift(@ARGV);
  my (%msg) = do_checkout(0, @ARGV);

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'archive')
{
  shift(@ARGV);
  my $format;
  my $outfile;
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($ok,$okcnt);
  my (%group,%msg);

  for(my $i=0; $i <= $#ARGV; $i++)
  {
    # OK, not the sanest options parse in the world, but it should be
    # close enough for sane developers to use.
    if ($ARGV[$i] eq "-l" || $ARGV[$i] eq "--list")
    {
      print "gits-tar\n";
      print `$git archive -l`;
      # explicit close will wait in case we have forked a pager
      close(STDOUT);
      exit(0);
    }
    if ($ARGV[$i] eq "--format" && $i < $#ARGV)
    {
      $format = $ARGV[$i+1];
      $ARGV[$i+1] = "tar" if ($format eq "gits-tar");
    }
    if ($ARGV[$i] =~ /^--format=(.*)/)
    {
      $format = $1;
      $ARGV[$i] =~ s/gits-tar/tar/ if ($format eq "gits-tar");
    }
    if (($ARGV[$i] eq "-o" || $ARGV[$i] eq "--output") && $i < $#ARGV)
    {
      $outfile = $ARGV[$i+1];
    }
    if ($ARGV[$i] =~ /^--output=(.*)/)
    {
      $outfile = $1;
    }
  }

  if ($format eq "gits-tar" && !$outfile)
  {
    die "Must provide a --output file with gits-tar archive format\n";
  }
  if ($format eq "gits-tar" && $outfile)
  {
    my $prefix;
    my $cmdargs;

    for(my $i=0; $i <= $#ARGV; $i++)
    {
      if ($ARGV[$i] eq "--prefix" && $i < $#ARGV)
      {
	$prefix=$ARGV[++$i];
	next;
      }
      if ($ARGV[$i] =~ /--prefix=(.*)/)
      {
	$prefix=$1;
	next;
      }
      if (($ARGV[$i] eq "--output" || $ARGV[$i] eq "-o") && $i < $#ARGV)
      {
	$i++;
	next;
      }
      next if ($ARGV[$i] eq "--output=(.*)");
      $cmdargs .= quoteit($ARGV[$i])." ";
    }

    $prefix .= "/" if ($prefix && $prefix !~ m:/$:);
    my $first = 1;
    my $curout = $outfile;
    foreach my $group (@list)
    {
      my $slave = $group->[1];
      my $repo = $slave;
      $repo = $TOP if ($repo eq ".");

      next if missing_slave($slave);

      my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git archive $cmdargs --prefix ${prefix}$slave/ --output $curout^,$slave);
      if ($ret == 0)
      {
	if ($first)
	{
	  undef($first);
	  $curout = ($ENV{'TMPDIR'}||'/tmp')."/gitslave-archive-tmp.$$";
	}
	else
	{
	  my($ret1, $msg1) = getcmd("tar -Af $outfile $curout");
	  unlink($curout) if ($ret1 == 0);
	  $ret = $ret1;
	  $msg .= $msg1;
	}
      }

      if ($ret != 0)
      {
	chomp($msg);
	my $warn = "gits archive failed for: '$repo': " . exitcode($ret);

	if ($OPTIONS{'keep-going'})
	{
	  warn "Error in $warn (continuing)\n $msg\n";
	  $returncode = 2;
	  next;
	}

	my $diemsg = "Aborting $warn\n ";
	if ($ok)
	{
	  $diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
	}
	else
	{
	  $diemsg .= "(first entry, no other successfully executed)";
	}
	die "$diemsg\n  $msg\n";
      }
      $ok .= " $repo";
      $okcnt++;


      push(@{$msg{$msg}}, $repo);
    }
  }
  else
  {
    foreach my $group (@list)
    {
      my $slave = $group->[1];
      my $repo = $slave;
      $repo = $TOP if ($repo eq ".");

      next if missing_slave($slave);

      my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git archive ^.quoteit(@ARGV),$slave);
      if ($ret != 0)
      {
	chomp($msg);
	my $warn = "gits archive failed for: '$repo': " . exitcode($ret);

	if ($OPTIONS{'keep-going'})
	{
	  warn "Error in $warn (continuing)\n $msg\n";
	  $returncode = 2;
	  next;
	}

	my $diemsg = "Aborting $warn\n ";
	if ($ok)
	{
	  $diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
	}
	else
	{
	  $diemsg .= "(first entry, no other successfully executed)";
	}
	die "$diemsg\n  $msg\n";
      }
      $ok .= " $repo";
      $okcnt++;

      push(@{$msg{$msg}}, $repo);
    }

  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'exec')
{
  shift(@ARGV);
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($branch);
  my (%group,%msg);

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && ^.quoteit(@ARGV),$slave);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits exec $ARGV[0], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn (continuing)\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;

    push(@{$msg{$msg}}, $repo);
  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'remote')
{
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($branch);
  my (%group,%msg);
  my ($realurl);
  my ($fromcheckout);

  if ($ARGV[1] eq "add")
  {
    $fromcheckout = 0;
    if (grep(/^--fromcheckout$/,@ARGV))
    {
      $fromcheckout = 1;
      @ARGV = grep(!/^--fromcheckout$/,@ARGV);
    }

    if (grep(/^--no-fromcheckout$/,@ARGV))
    {
      $fromcheckout = 0;
      @ARGV = grep(!/^--no-fromcheckout$/,@ARGV);
    }

    $realurl = $ARGV[$#ARGV];
  }

  # ARGV[2] better be a remote!
  set_fromcheckout($fromcheckout,$ARGV[2]) if (defined($fromcheckout));


  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    if ($ARGV[1] eq "add")
    {
      $ARGV[$#ARGV] = resolve_repository($group->[0],$group->[1],$ARGV[2],$realurl);
    }

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git ^.quoteit(@ARGV),$slave);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits remote $ARGV[1], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn (continuing)\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;

    push(@{$msg{$msg}}, $repo);
  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'update-remote-url')
{
  my ($fromcheckout);

  $fromcheckout = 0;
  if (grep(/^--fromcheckout$/,@ARGV))
  {
    $fromcheckout = 1;
    @ARGV = grep(!/^--fromcheckout$/,@ARGV);
  }

  if (grep(/^--no-fromcheckout$/,@ARGV))
  {
    $fromcheckout = 0;
    @ARGV = grep(!/^--no-fromcheckout$/,@ARGV);
  }

  if ($#ARGV != 2 || !$ARGV[1] || !$ARGV[2])
  {
    my $example = `$git config remote.origin.url`;

    if ($example)
    {
      my ($from) = $fromcheckout?"--fromcheckout":"--no-fromcheckout";
      $example = "Example: gits update-remote-url $from origin $example\n"
    }

    die "gits update-remote-url requires two non-option arguments\nUsage: gits update-remote-url [--[no-]fromcheckout] REMOTENAME NEWURL\n$example";
  }

  my ($cmd,$name,$url) = @ARGV;
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my (%msg);

  set_fromcheckout($fromcheckout,$name) if (defined($fromcheckout));

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($thisurl) = $url;
    if ($slave ne ".")
    {
      $thisurl = resolve_repository($group->[0],$group->[1],$name,$url,$group->[2]);

      print qq^For slave $slave, "@{[$group->[0]]}" "@{[$group->[1]]}" $name $url  "@{[$group->[2]]}" => $thisurl\n^;
    }

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git config "remote.$name.url" "$thisurl"^,$slave);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits $cmd, failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;

    push(@{$msg{$msg}}, $repo);
  }

  stdout(\%msg);
  set_fromcheckout($fromcheckout, $name);
}
##################################################
elsif ($ARGV[0] eq 'status')
{
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($branch);
  my (%group,%groupc,%msg);

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg) = getcmd(qq^cd "$slave" && $git ^.quoteit(@ARGV),$slave);
    if ($ret != 0 && $ret != 256)
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    elsif ($ret == 256)
    {
      # git status seems to return 1 (nee wait 256) under all circumstances, so do
      # not even propagate this condition.
      #$returncode = 1;
      1;
    }
    $ok .= " $repo";
    $okcnt++;

    my ($premove);
    die "gits unexpected status output (missing branch): $msg" unless ($msg =~ s/^(?:# )?(?:On branch |Not currently on any branch.)(.+)?\n?//);   
   
    my $localbranch = $1 ? $1 : "(no branch)";
    $branch = $localbranch unless ($branch);
    unless ($branch eq $localbranch)
    {
      our %wrongbranches;
      # turn on minimal verbosity to get empty messages in matching repositories
      # (this allows "all other repositories" summarization)
      $OPTIONS{'verbose'} = "0e0" if (!$OPTIONS{'verbose'});
      my $warning = "Top-level $TOP branch '$branch' != slave branch '$localbranch'!\n";
      $msg = $warning . $msg;
      warn $warning if (!$wrongbranches{$localbranch});
      $wrongbranches{$localbranch} = 1;
    }

    if ($msg =~ /^\# Your branch/)
    {
      while ($msg =~ s/^(\# \S.*\n)//)
      {
	$premove .= $1;
      }
      $msg =~ s/^\#\s*\n//;
    }

    $msg =~ s/^no(thing| changes)( added)? to commit .*\n//m;

    while ($msg =~ s/^(\# \S.*\n)//)
    {
      my ($group) = $1;
      while ($msg =~ s/^(\#\s{2,}\S.*\n)//)
      {
	$groupc{$group}->{$1} = 1;
      }
      die "Could not parse git status output for $slave <$group> <$msg>\n" unless ($msg =~ s/^(\#\s*\n)//);

      while ($msg =~ s/^(\#(?:\s*|(\s{2,}|\t)\S.*)\n)//)
      {
	my ($line) = $1;

	if ($line =~ /([^:]+:\s+)(.*)/s)
	{
	  $line = "$1$slave/$2";
	}
	elsif ($line =~ /(\#\s+)(.+)/)
	{
	  $line = "$1$slave/$2 # Do not git(s) add this path\n";
	}
	else
	{
	  next if ($line =~ /\#\s*\n/);
	}

	$group{$group} .= $line;
      }
    }

    push(@{$msg{$premove.$msg}}, $repo);
  }

  print "# On branch $branch\n";
  foreach my $group (sort keys %group)
  {
    print $group;
    foreach my $msg (sort keys %{$groupc{$group}})
    {
      print $msg;
    }
    print "#\n";
    print $group{$group};
    print "#\n";
  }

  stdout(\%msg,1);
}
##################################################
elsif ($ARGV[0] eq 'statuses')
{
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my $oldbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd("$git branch --no-color"))))[0];
  my @branches = grep(s/branch\.(.*)\.remote=.*/$1/,split(/\n/,getcmd(qq%$git config -l%)));
  my @oldbranch = ($oldbranch);

  shift(@ARGV);
  my $move;
  if ($ARGV[0] eq '-m')
  {
    shift(@ARGV);
    unshift(@oldbranch, '-m');
    $move = '-m';
  }

  my $firstbranch = 1;

  foreach my $branch (@branches)
  {
    my %newmsg = $move
      ? do_checkout(1, $oldbranch, $move, $branch)
	: do_checkout(1, $oldbranch, $branch);

    my (%group,%groupc,%msg);

    foreach my $group (@list)
    {
      my $slave = $group->[1];
      my $repo = $slave;
      $repo = $TOP if ($repo eq ".");

      next if missing_slave($slave);

      my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git status ^.quoteit(@ARGV),$slave);
      if ($ret != 0 && $ret != 256)
      {
	chomp($msg);
	my $warn = "gits statuses, failed on branch $branch for: '$repo': " . exitcode($ret);

	if ($OPTIONS{'keep-going'})
	{
	  warn "Error in $warn\n $msg\n";
	  $returncode = 2;
	  next;
	}

	my $diemsg = "Aborting $warn\n ";
	if ($ok)
	{
	  $diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
	}
	else
	{
	  $diemsg .= "(first entry, no other successfully executed)";
	}
	# <TODO>some way for do_checkout to only operate on some slaves</TODO>
	$msg .= do_checkout(2, @oldbranch);
	die "$diemsg\n  $msg\n";
      }
      elsif ($ret == 256)
      {
	# git status seems to return 1 (nee wait 256) under all circumstances, so do
	# not even propagate this condition.
	#$returncode = 1;
	1;
      }
      $msg = $newmsg{$slave}.$msg;
      $msg =~ s/Switched to branch.*\n//;
      $msg =~ s/Already on \".*\n//;
      $ok .= " $repo";
      $okcnt++;

      my ($premove);
      unless ($msg =~ s/^(?:# )?(?:On branch |Not currently on any branch.)(.+)?\n?//)
      {
	# <TODO>some way for do_checkout to only operate on some slaves</TODO>
	$msg .= do_checkout(2, @oldbranch);
        die "gits unexpected status output (missing branch): $msg";
      }

      if ($msg =~ /^\# Your branch/)
      {
	while ($msg =~ s/^(\# \S.*\n)//)
	{
	  $premove .= $1;
	}
	$msg =~ s/^\#\s*\n//;
      }

      $msg =~ s/^no(thing| changes)( added)? to commit .*\n//m;

      while ($msg =~ s/^(\# \S.*\n)//)
      {
	my ($group) = $1;

	while ($msg =~ s/^(\#\s{2,}\S.*\n)//)
	{
	  $groupc{$group}->{$1} = 1 if ($firstbranch);
	}

	unless ($msg =~ s/^(\#\s*\n)//)
	{
	  # <TODO>some way for do_checkout to only operate on some slaves</TODO>
	  my $c = do_checkout(2, @oldbranch);
	  die "Could not parse git status output for $slave <$group> <$msg>$c\n"
	}
	while ($msg =~ s/^(\#(?:\s*|(\s{2,}|\t)\S.*)\n)//)
	{
	  next unless ($firstbranch);

	  my ($line) = $1;

	  if ($line =~ /([^:]+:\s+)(.*)/s)
	  {
	    $line = "$1$slave/$2";
	  }
	  elsif ($line =~ /(\#\s+)(.+)/)
	  {
	    $line = "$1$slave/$2 # Do not git(s) add this path\n";
	  }
	  else
	  {
	    next if ($line =~ /\#\s*\n/);
	  }

	  $group{$group} .= $line;
	}
      }

      push(@{$msg{$premove.$msg}}, $repo);
    }

    if ($firstbranch)
    {
      $firstbranch = 0;
      foreach my $group (sort keys %group)
      {
	print $group;
	foreach my $msg (sort keys %{$groupc{$group}})
	{
	  print $msg;
	}
	print "#\n";
	print $group{$group};
	print "#\n";
      }
    }

    print "# On branch $branch\n";
    stdout(\%msg,1);
  }
  print do_checkout(2, @oldbranch) . "\n";
}
##################################################
elsif ($ARGV[0] eq 'grep')
{
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($out);

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git ^.quoteit(@ARGV),$slave);
    next if ($ret == 256);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }

    $ok .= " $repo";
    $okcnt++;

    $msg =~ s:^(.*\S.*\n):$slave/$1:gm unless (grep(/-h/,@ARGV));

    $out .= $msg;
  }

  print $out;
}
##################################################
elsif ($ARGV[0] eq 'clean')
{
  shift(@ARGV);
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my (@unlinkers);
  my (%msg);
  my ($commonout);

  my $CLEAN_USAGE = "Usage: gits clean -f|-n [-dqxX] [--] [PATH]...\n";
  my %COPTIONS;
  GetOptions(\%COPTIONS, 'd', 'f', 'q', 'n', 'X', 'x') || die $CLEAN_USAGE;

  die "Must set exactly one of -n or -f\n$CLEAN_USAGE" unless (!$COPTIONS{'f'} != !$COPTIONS{'n'});

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($cmd) = qq^cd "%%dir%%" && $git clean -n^;
    $cmd .= " -d" if ($COPTIONS{'d'});
    $cmd .= " -f" if ($COPTIONS{'f'});
    $cmd .= " -X" if ($COPTIONS{'X'});
    $cmd .= " -x" if ($COPTIONS{'x'});
    $cmd .= " -- ".quoteit(@ARGV) if ($#ARGV >= 0);

    my ($ret, $msg) = getcmd($cmd,$slave);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;

    my $newmsg;
    foreach my $line (split(/\n/,$msg))
    {
      if ($line =~ s/(Would( not)? remove )(.*)//)
      {
	my ($pre,$not,$path) = ($1,$2,$3);

	# Canonicalize
	$path = "$slave/$path";
	$path =~ s:^./::;

	# Check directory to see if it matches slavedir
	if ($path =~ s:/$::)
	{
	  my ($qpath) = quotemeta($path);

	  next if grep($_->[1] =~ m:^$qpath$:, @slaves);

	  if (-d "$path/.git")		# Don't remove directories with .git in them.
	  {
	    $commonout .= "Skipping $path (contains .git)\n";
	    next;
	  }

	  # Restore trailing /
	  $path .= "/";
	}
	if ($COPTIONS{'n'})
	{
	  $commonout .= $pre.$path."\n";
	  next;
	}
	next if ($not);
	$commonout .= "Removing $path\n";
	push(@unlinkers,$path);
      }
      else
      {
	$newmsg .= $_;
      }
    }

    push(@{$msg{$newmsg}}, $repo);
  }

  print $commonout unless ($COPTIONS{'q'});

  if (!$COPTIONS{'n'} && $#unlinkers >= 0)
  {
    # clean up Carp::carp messages from File::Path rmtree
    open(STDERR, "| sed 's/ at [^ ]*gits line [0-9]*\$//'");
    eval { rmtree(\@unlinkers, $OPTIONS{'verbose'}); };
    die "$@\n" if ($@);
    foreach my $path (@unlinkers)
    {
      die "Could not remove all paths\n" if (-e $path);
    }
    close(STDERR);
  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'logs')
{
  shift(@ARGV);
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my (%group,@msg,$out);
  my ($window) = 28800;				# 8 hours

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git log --date=relative --pretty='tformat: %at %h %ae %ad: %s' ^.quoteit(@ARGV),$slave);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits exec log, failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn (continuing)\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;

    foreach my $line (split(/\n/,$msg))
    {
      chomp($slave);
      chomp($line);
      warn "Bad entry on $slave" unless ($line =~ /^\s*(\d+)\s+([[:xdigit:]]+)\s+(\S+)\s+(.*)/);
      push(@msg, [$1,$2,$repo,$3,$4]);
    }
  }

  @msg = sort { my $x; return($x) if ($x = $b->[0] <=> $a->[0]); $b->[2] cmp $a->[2]; } @msg;

  while (my $line = shift(@msg))
  {
    $out = "$line->[4] <$line->[3]>\n\t$line->[1] $line->[2]\n";
    my $i = 0;
    my $lower = $line->[0] - $window;

    for(;$i <= $#msg && $msg[$i]->[0] > $lower;$i++)
    {
      # Require matching committer
      next if ($line->[4] ne $msg[$i]->[4]);

      # Require matching commit message
      next if ($line->[5] ne $msg[$i]->[5]);

      $out .= "\t$msg[$i]->[1] $msg[$i]->[2]\n";
      $lower = $msg[$i]->[0] - $window;
      splice(@msg,$i--,1);			# Get rid of item we already matched
    }
    print "$out\n";
  }
}
##################################################
else # Generic command
{
  my ($ok,$okcnt);
  my %msg;

  # This is a sign that you are doing something wrong
  if (-f $ARGV[$#ARGV])
  {
    warn "There is a pathname in a generic gits command; this is a sign that the command\nwill not work since it is unlikely to exist in all repositories.\nProbably you want to run a raw git command\n";
  }

  # Run the command for each slave
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});

  if ($ARGV[0] eq 'gc' || $ARGV[0] eq 'fsck')
  {
    $progress = Term::ProgressBar->new({remove=>1, ETA => 'linear', count=>(($#list+1))}) if ($want_progress);
    $progress->max_update_rate(1) if ($progress);
  }

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git ^.quoteit(@ARGV),$slave);
    if ($ret != 0 &&
	!($ARGV[0] eq "commit" && $msg =~ /nothing to commit/) &&
	!($ARGV[0] eq "stash" && $ARGV[1] eq "apply" && $msg =~ /no valid stashed state found/))
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	$warn = "Error in $warn (continuing)\n $msg\n";
	$warn = "\n$warn" if ($progress);
	warn $warn;
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      $diemsg = "\n$diemsg" if ($progress);
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;
    push(@{$msg{$msg}}, $repo);

    $progress->update(++$progress_count) if ($progress);
  }

  stdout(\%msg);
}
# explicit close will wait in case we have forked a pager
close(STDOUT);
exit($returncode);



=pod

Gitslave Home Page: L<< http://gitslave.sf.net >>

=head1 NAME

gits - The git slave repository tool for multi-repository management

=head1 SYNOPSIS

gits [-p|--parallel COUNT] [-v|--verbose]+ [--quiet] [--help] [--version]
[-n|--no-pager] [--paginate] [--eval-args] [--exclude SLAVE-REGEXP]
[--keep-going] [--no-commit] [--no-hide] [--no-progress]
[--no-master] [--with-ifpresent|--just-ifpresent] SUBCOMMAND [ARGS]...

=head1 OVERVIEW

gits is a program that assists in assembling a meta-project from a
number of individual git repositories which operate (when using gits)
as if they were one git repository instead of many, similar to the way
that CVS works by default and svn (v1.5) can be coerced to work.  Some
of these individual git repositories may be part of other
meta-projects as well.

Unfortunately, the functionality provided by git-submodule is not
sufficient for this mode of operation.  Most git commands, like
checkout or commit, do not recursively descend into the submodules so
you are forced to do execute all git commands N+1 times (leading to
pain and mistakes), also, submodules revisions are tracked in the
supermodule so that changes to a submodule made outside the
supermodule are not automatically seen.  Since git does not allow
partial checkouts, we are left with little alternative.

Thus, to solve these problems gits was born.  Complexity pain is still
involved, but the hope is that it is minimized compared to all of the
other alternatives.

The basic theory is that there are a few sub-commands (prepare, attach,
populate) which help set up the meta-project for gits operations.
Then, except for git commands with specific filenames, like git add
FILENAME; git reset FILENAME; etc., you should use "gits" instead
of "git", and the command will run on all repositories in the project.

=head2 Example Usage

In the following example, we will have the following master
repositories:

  ssh://sourcemaster/src/repos/super.git
  ssh://sourcemaster/src/repos/lib1.git
  ssh://sourcemaster/src/repos/lib2.git


The desired working layout of directories with .git in them on disk
is:

  ..../super
  ..../super/lib1
  ..../super/lib2



=head3 Clone a gits project

gits clone [--[no-]fromcheckout] [--nohooks] [--reference[checkout]=PATH] SUPERPROJECT-URL

This command clones an existing superproject and all associated slave
repositories.  It runs `git clone` on the arguments you provide
(allowing you to change the name of the checked-out repository for
example) and then runs `gits populate` inside that newly checked out
repository.  If you are cloning from an existing gitslave checkout
instead of the master layout (the master layout is specified in the
gits attach commands), you would want to use the --fromcheckout
argument.

By default, if there is a git-hooks directory present in the superproject,
hooks there will be symlinked into the .git/hooks directory in all related
repositories.  Adding --nohooks will disable this management.

  --------------------------------------------------
  gits clone ssh://sourcemaster/src/repos/super.git super2
  cd super2
  --------------------------------------------------

The gits option --with-ifpresent can be used with this command to populate
all conditional slave repositories (those marked with ifpresent flag).

The clone options --reference=PATH or --referencecheckout=PATH work
like the `git clone --reference` option, modifying the path based on
the normal rules (the --fromcheckout rules are used with
--referencecheckout) for upstream repository construction.  You must
use equals (=) to separate the PATH argument from the option, you
cannot use whitespace as allowed by git.

The purpose of the --reference options is to avoid lengthy network
copies if you already have a local repository.  Note that the use of
shared repositories created by --reference is potentially dangerous;
see the notes on --shared in the git-clone man page and consider
running `gits repack -a` to remove these linkages.



=head3 Initialize a gits project

gits prepare

You run this command in the git directory which will be your top level
master repository (super here).  Typically you would clone this top
level master from some other location which has all of your git
projects.

  --------------------------------------------------
  git clone ssh://sourcemaster/src/repos/super.git super
  cd super
  gits prepare
  --------------------------------------------------



=head3 Add a slave repository to top level master

  gits attach [--recursive=FILENAME] [--reference=PATH] [--adminonly] REPOSITORY LOCALPATH [FLAGS]

Clone the named git repository into the named local directory, and set
it up for further gits operations.

Typically LOCALPATH would be a path relative to the top level
working directory, for example, a subdirectory in the top level.

REPOSITORY can also be relative (relative to the URL the top level
master checkout was cloned from).  It may also be an absolute URL but
if so, `gits remote add` is not going to be happy.  If the URL starts
with ^, it will use only the method and hostname from the master's
URL.  Otherwise, it will be relative to the fully qualified path.  We
will show both in operation in the example.

The only [flags] currently supported is "ifpresent" which will be set for
this slave repository.  Other people cloning or populating the
superproject will not check out this subrepository if this flag is
set, unless they add --with-ifpresent or otherwise arrange for the
LOCALPATH to be created.

If the --recursive=FILENAME argument is present (and you I<must> use
the --recursive=FILENAME style, not "--recursive FILENAME") the
repository you are attaching will be treated as a recursive gitslave
master underneath the top-level gitslave master.  The filename will
typically be ".gitslave".  It is relative to the localpath
you checked out and must not have any / in it.

The attach option --reference=PATH works exactly like the `git clone
--reference` option.  In fact this option will be passed as-is to the
`git clone` command.  You must use equals (=) to separate the PATH
argument from the option, you cannot use whitespace as allowed by git.

The purpose of the --reference option is to avoid lengthy network
copies if you already have a local repository.  Note that the use of
shared repositories created by --reference is potentially dangerous;
see the notes on --shared in the git-clone man page and consider
running `gits repack -a` to remove these linkages.

If the --adminonly option is given, this tells gitslave to set up the
management files and to NOT clone.  You must clone through a
subsequent prepare or some manual action.

 --------------------------------------------------
 gits attach ../lib1.git lib1
 gits attach ^/src/repos/lib2.git lib2
 gits attach --recursive=.gitslave ../../super2 supersub
 gits push
 --------------------------------------------------

The push in the example is to share the attach with other users.



=head3 Checkout any slave repositories that may have been added

  gits populate [--[no-]fromcheckout] [--nohooks] [--reference[checkout]=PATH] [SLAVES]...

Go through the list of configured slaves and check out (clone) any which have
not already been retrieved.

With --fromcheckout, assume that the remote repository is a gits checkout
instead of in standard repository layout.  With --no-fromcheckout, assume
that the remote repository has the standard layout.  If either option is
given, sets the default repository layout, which is used when no explicit
option is provided.

The gits option --with-ifpresent can be used with this command to populate
all conditional slave repositories (those marked with ifpresent flag).  You
can also populate particular conditional slave repositories by listing them
on the command line.

By default, if there is a git-hooks directory present in the superproject,
hooks there will be symlinked into the .git/hooks directory in all related
repositories.  Adding --nohooks will disable this management.

The populate options --reference=PATH or --referencecheckout=PATH work
like the `git clone --reference` option, modifying the path based on
the normal rules (the --fromcheckout rules are used with
--referencecheckout) for upstream repository construction.  You must
use equals (=) to separate the PATH argument from the option, you
cannot use whitespace as allowed by git.

The purpose of the --reference options is to avoid lengthy network
copies if you already have a local repository.  Note that the use of
shared repositories created by --reference is potentially dangerous;
see the notes on --shared in the git-clone man page and consider
running `gits repack -a` to remove these linkages.

  --------------------------------------------------
  gits populate
  --------------------------------------------------



=head3 Safely delete local repositories that are no longer wanted

  gits release [-n] --all | --just-ifpresent | SUPERPROJECT | SLAVES...

Go through the list of configured slaves (or slave arguments) and confirm
that there are no unresolved conflicts, untracked (and not ignored) files,
uncommitted changes (including staged or stashed changes), unpushed
commits, or unmerged private branches, and that there is a tracking branch
in each repository.  If these conditions are met in I<all> of the selected
slaves, the selected slave directories are removed (rm -rf).

The --no-commit (or -n) option prevents repositories from being removed.
(Note that if -n is passed to 'gits' instead of the 'release' subcommand,
it also disables automatic pagination as --no-pager).  This option is
implied if the superproject is the root directory (/) or if paths to any
slave repositories start with dot (.), contain '..' (parent directory)
components, or are absolute (start with /).

The --just-ifpresent option can be used to release only conditional slave
repositories (marked with ifpresent flag).  This option cannot be used with
slave repository arguments or --all.

The --all option releases the entire current superproject; unless -n,
--no-commit, or --no-master is used, the current directory is removed,
breaking relative paths until you change directory.  Instead, you can pass
one superproject as an argument with an absolute path (starting with /);
this will work from any directory.

Other arguments are treated as the names of slave repositories to release.
Only one recursive superproject can be specified, and only as an absolute
path. Slave arguments cannot start with dot (.)  or contain certain shell
metacharacters (semicolon, newline, or quotes).  Slave arguments cannot be
combined with --all or --just-ifpresent options.

The --force option can be used to force removal even if the state of some
repositories does not meet the conditions noted above.  This is I<EXTREMELY
DANGEROUS> and roughly equivalent to rm -rf on the arguments; for this
reason the --force option cannot be combined with --all or a superproject
argument.

  --------------------------------------------------
  gits release --just-ifpresent
  gits release lib1
  gits release -n --all
  gits release /path/to/super2
  --------------------------------------------------



=head3 Remove a slave repository from top level master

  gits detach [--force] SLAVE

Do a `gits release` on SLAVE; if this succeeds, besides removing the slave
repository from the local filesystem, also remove the slave from .gitignore
and the .gitslave management file, so that it will not be used in
subsequent gits activities.

The --force option can be used to override the gits release check; if this
is done, the repository will not be removed - only .gitignore and .gitslave
will be affected.

The --no-commit option will only remove the slave from .gitignore and
.gitslave; those changes will not be committed, and the slave repository
will not be removed from the local filesystem.

 --------------------------------------------------
 gits detach lib1
 gits push
 --------------------------------------------------

The push in the example is to share the detach with other users.



=head3 Perform a pull operation for all tracked branches

  gits pulls [pull args]

For each branch being tracked by the superproject, go through the list
of configured slaves, check yourself out to the branch, perform a pull,
then switch back to the branch you were on.

Please see SUBSTITUTION for information on how to replace part of the
command with the slave repository name for each executed command.

In the common case where no repository or refspec is provided as an
argument, pulls will perform a fetch once (in the current branch of
each slave) and then rebase (with --preserve-merges) or merge in each
branch.  This reduces redundant network overhead in the common case
where all branches of a repository have the same remote, but if that
is not the case, only the current remote will be fetched.  If you want
to force the slower operation of a pull in each branch, pass the --
argument, e.g. gits pulls --rebase --

  --------------------------------------------------
  gits pulls --rebase
  --------------------------------------------------



=head3 Perform a pull operation for the current branch only

  gits pull [pull args]

Go through the list of configured slaves and perform a pull,  Note that
even though only the current branch HEAD will be advanced, the commits
on other branches will still be fetched.

Please see SUBSTITUTION for information on how to replace part of the
command with the slave repository name for each executed command.

  --------------------------------------------------
  gits pull --rebase
  --------------------------------------------------
  gits pull otherhost:/src/work/wb/%%dir%%
  --------------------------------------------------



=head3 Show unified commit logs in a fixed output format

  gits logs [log args]

For each branch being tracked by the superproject, generate a list
of the "log" messages as specified by the log args, and output them in
a fixed format, with related commits grouped together and ordered by
time. Do I<not> provide git log options that modify the output format,
as they will break the ordering and grouping functionality; only
arguments that control the selection of commits should be used.
(Note that using the --date={relative,local,default,iso,rfc,short}
option is okay, and will modify the displayed date format).

The related commit grouping will group together all commits (in any
sub-project) within an 8-hour period that have the same author
e-mail and commit message.

  --------------------------------------------------
  gits logs HEAD...Product-3.1.1
  --------------------------------------------------



=head3 Perform an arbitrary command for all tracked branches

  gits exec COMMAND [ARGS]

For each slave being tracked by the superproject and the superproject itself,
cd to the slave directory and execute the listed command.

Please see SUBSTITUTION for information on how to replace part of the
command with the slave repository name for each executed command.

  --------------------------------------------------
  gits exec gitk
  --------------------------------------------------
  gits exec git diff
  --------------------------------------------------



=head3 Print out URLs for arbitrary repositories like those used by attach

  gits resolve [--[no-]fromcheckout] REPOSITORY LOCAL_RELPATH [REMOTE]

Go through the same process that gits uses for resolving relative
repository URLs into absolute URLs, for debugging and certain porting
efforts.  You may specify the remote of the superproject you wish the
relative repository to be in relation to: by default it uses origin.

With --fromcheckout, resolve URLs as if the repository were a clone of
another gits checkout.  With --no-fromcheckout, resolve URLs as if it were
not a clone of another gits checkout.  By default it uses the saved
repository layout from gits populate or update-remote-url; however gits
resolve does not change the saved default even if --fromcheckout or
--no-fromcheckout is given.

  --------------------------------------------------
  gits resolve ../otherpos otherpos
  gits resolve ../otherpos otherpos otherremote
  --------------------------------------------------



=head3 Update the URL a remote repository points at

  gits update-remote-url [--[no-]fromcheckout] REMOTENAME NEWURL

Update the superproject's remote.REMOTENAME.url to be NEWURL.  Then go though
each slave repository and update its remote.REMOTENAME.url using the normal
relative url mechanism.

The --fromcheckout option supports using a gits checkout as the remote
repository, adjusting the repository paths from their default.  The
--no-fromcheckout option assumes a normal (as specified in the gits
attach commands) repository layout for the remote. If neither option
is given, the saved repository layout from the most recent gits
populate or update-remote-url command is used; however, this is not
generally correct, as the repository layout of the new remote need not
be the same as the old one.  If either --fromcheckout or
--no-fromcheckout is given, sets the default repository layout
accordingly.

You can use this command after a clone of a local repository (or local gits
checkout, using gits populate --fromcheckout) so that the new repository
uses the same remote origin as the first one.  (The local clone/populate is
much faster than performing a remote clone/populate.)

  --------------------------------------------------
  git clone /home/user/work/wb /home/user/work/newwb
  cd /home/user/work/newwb
  gits populate --fromcheckout
  gits update-remote-url --no-fromcheckout origin ssh://git/src/git/wb
  --------------------------------------------------



=head3 Add a new remote to all repositories

  gits remote add [--[no-]fromcheckout] GIT-REMOTE-ADD-OPTS REMOTENAME REMOTEURL

Adds a remote named REMOTENAME for the repository at REMOTEURL (as modified by
the relative URL rules from `gits attach` and `gits update-remote-url`
and the --fromcheckout argument). The command gits fetch REMOTENAME can
then be used to create and update remote-tracking branches matching
REMOTENAME/*.

The superproject remote URL will be set to REMOTEURL, which should be
an absolute URL; the slave repositories will have a modified version
of that URL (the layout must match that of the existing remotes).

Please note that we do not currently support `gits remote set-url` in
a useful way.  See `gits update-remote-url` for an alternate method
which will satisfy most use cases.

 --------------------------------------------------
 gits remote add fred --fromcheckout /home/fred/src/foo
 --------------------------------------------------
 gits remote add backup ssh://backups/src/git/foo
 --------------------------------------------------



=head3 Push a change to a remote repository

  gits push [--quick] [push args]

This is the standard git push command--with the addition of a --quick
option which will only attempt to push for branches which have
outstanding changes.  For slow connections to large number of slave
repositories, the overhead of an empty push can be large.

However, --quick only checks to see if the I<current> branch needs to
push data.  If you have changes on other branches, a slow push is
still required, as it is if you are pushing to a repository other than
the standard origin.

Please see SUBSTITUTION for information on how to replace part of the
command with the slave repository name for each executed command.

  --------------------------------------------------
  gits push --quick
  --------------------------------------------------
  gits push otherhost:/src/work/wb/%%dir%%
  --------------------------------------------------



=head3 Get status on all branches

  gits statuses [-m] [status args]

For each branch being tracked by the superproject, go through the list
of configured slaves, check yourself out to the branch, get the git
status, then switch back to the branch you were on.  The output is
summarized for each branch to merge the lists of files in each section.

The -m option will attempt to "move" any uncommitted changes, which
may prevent failures checking out other branches, at the risk of
creating conflicts, which are then moved as well

Other args supported by `git status` are also supported; these are
the same options supported by `git commit`.

  --------------------------------------------------
  gits statuses
  --------------------------------------------------



=head3 Make an archive of the repositories

  gits archive GIT-ARCHIVE-ARGS

This is the standard git archive command--with the addition of a new
--format option of "gits-tar".  When you select the gits-tar option,
you must supply an on-disk --output file and you cannot use any tar
compression options.  With gits-tar, the output tar archive will be a
unified archive of the entire project, superproject and slaves.  Any
existing prefix will be treated as a directory prefix (e.g. --prefix
foo and --prefix foo/ are the same) and all slave repositories will be
unpacked in their corresponding superproject locations.

If you choose a format other than gits-tar, you will probably want to
use one of the substitutions like %%basename%% or %%dir%% in the
output filename.

  --------------------------------------------------
  gits archive --format gits-tar -o /tmp/foo.tar master
  --------------------------------------------------
  gits archive --format tar -o /tmp/foo-%%basename%%.tar master
  --------------------------------------------------



=head3 Everything else

All other commands are passed directly though to git, with one command
being run per repository.  Output summarizing is performed so that
multiple repositories with the same git output will only have the git
output shown once.  `git status` has a more aggressive summarizing
to merge the lists of files in each section.

  Examples:
  --------------------------------------------------
  gits commit -a -m "This is a change"
  gits push
  gits pull
  gits branch testing
  gits checkout testing
  gits diff master testing
  gits status
  gits ....
  --------------------------------------------------

All normal git commands are supported (plus any potential future
commands) but not all commands make sense to run with gits.  One good
example is git-daemon.

=head1 DESCRIPTION

=head2 --parallel=COUNT -p COUNT

Specify the number of parallel git operations you wish to execute.
Parallelism is only activated for push and pull(s) subcommands.
This can speed up your processing significantly for large
numbers of slave repositories.

If remote repositories are accessed over ssh, you may also wish to
activate ssh "ControlMaster" multiplexing.

  ssh_config
  --------------------------------------------------
  Host git
     ControlMaster auto
     ControlPath ~/.ssh/master-%r@%h:%p
  --------------------------------------------------

However, there currently is a "auto" race condition so the first batch
of peers do not necessarily take advantage of the multiplexing and
there have occasionally been spurious errors with this enabled.

=head2 --verbose -v

Ask for more information about what is happening.  You may repeat the
flag multiple times to get more information.  One level of verbosity
(-v) will print some minor warnings and will specify every repository
and its output explicitly.  Two levels of verbosity (-vv) will print
the underlying commands being executed and three levels (-vvv) will
print the data being returned from them.

=head2 --quiet

Ask for less information, which currently means discarding the STDERR
of some of the administrative git commands which are executed.

=head2 --rawout

Do not go through output summariziation ("On repo" and leading spaces)
for most gitslave commands.  Note that the output may currently not be
in the same repository order as it was originally.  Implies --no-hide

Often used for post-processing output.  Example:

  rsync -aR `gits --rawout exec sh -c 'git ls-tree -r --name-only HEAD | sed "s:^:%%dir%%/:"'` otherhost:/otherdir

=head2 --help

Print gits usage summary and details of gits options and substitutions
from this documentation, then exit.

=head2 --version

Print version information for gits, git, and Perl.

=head2 --paginate --no-pager -n

Disable or re-enable default pagination of gits output using a pager.
The pager (default is less) and its options are configured just as for
git itself (using core.pager in the master repository or global settings,
or environment variables PAGER or GIT_PAGER, with the same precedence).

Pagination is only enabled if standard output is a tty (and there is a
controlling tty for the pager to take input from).  Pagination is
disabled if the configured pager (from config and environment) is set
to the value "cat" or the empty string.

The -n option will disable pagination, but will be overridden by any
--paginate or --no-pager arguments present on the command line, even if
the -n option is given later.

=head2 --eval-args

When quoting gits arguments, do not quote dollar '$' and backtick '`'
characters, to allow interpolation in the slave environment (but still
quote double-quote and backslash).  This is mostly useful for exec,
where you might want something like the command below to do something
useful.

  gits --eval-args exec echo Directory is '`pwd`'

=head2 --exclude=SLAVE-REGEXP

Provide a regular expression which excludes those slaves from
consideration from gits commands which it matches.

=head2 --keep-going

Do not abort when any subsidiary git command fails - instead print a
warning and continue processing.  Some git command failures will still
be considered fatal and cause gits to abort.

=head2 --no-commit

This flag requests that gits-internal sub-commands, such as prepare or
attach, should not commit their changes after they make them.

=head2 --no-hide

This flag requests that gits not hide information when similar (but not
identical) output such as commit hashes is output for slave repositories.

=head2 --no-progress

This flag requests that gits NOT print a progress bar, which it does
by default for slow operations if Term::ProgressBar is loaded.  Slow
operations are the checkout, fetch, pull(s), and push subcommands.
You may use this flag for all operations.

=head2 --no-master

This flag requests that gits only run the listed command on the slave
repositories and NOT the super/master/top repository.

=head2 --with-ifpresent

Operate also on those slave repositories which are marked as
"ifpresent" even if they are not present.  This is mostly useful for
`gits populate` and `gits checkout`.

=head2 --just-ifpresent

Operate I<only> on those slave repositories which are marked as
"ifpresent" whether they are present or not.  This is mostly useful for
`gits release`.  Note that this implies --no-master, and overrides
--with-ifpresent.

=head2 SUBCOMMAND [ARGS]...

Run the specified git command (with associated arguments) on the
repository and all slave repositories.  Typically they are git
commands run over each slave, but there are gits specific commands
such as: pulls, prepare, attach, populate, resolve, exec, logs, and
update-remote-url.  See OVERVIEW for more information on specific
subcommands.



=head1 SUBSTITUTION

Before execution, essentially all commands running over all of the
repositories will go through a substitution phase where certain magic
tokens will be replaced with information about the repository in
question.  These are most often used with `gits exec` and `gits
archive`.

%%dir%% represents the on-disk .gitslave-relative of the repository
(e.g. the second field in .gitslave) with the superproject getting the
value of ".".

%%path%% represents the fully qualified path to the repository in
question.

%%basename%% represents the basename of the %%path%%, which is
typically the last component of %%dir%% except for the superproject
for which it is whatever the last directory name of the path to the
superproject.

%%upstream%% represents the URL to the origin repository for the
repository in question.

%%upstream_base%% represents the basename to the URL to the origin
repository for the repository in question.

  # Create a shadow set of bare repositories locally w/o massive transfers from origin
  gits exec git clone --bare --reference=%%path%% %%upstream%% /tmp/r/%%upstream_base%%

Also see --eval-args for an option to support standard shell `cmd`
and $VARIABLE expansion where it might otherwise be quoted.  Run the
following commmands to see the difference:

  gits exec echo '`ls -ld $PWD`'

  gits --eval-args exec echo '`ls -ld $PWD`'

=head1 BUGS

gits changes directory to the directory where .gitslave exists.
Likewise, when executing most git commands, gits changes directory to
the root of the git slave, so any pathnames passed as arguments to
gits must be absolute, not relative.  Generally this is only a concern
for pre-generated commit messages or things like that; you should NOT
be passing gits the pathnames of files checked into git slaves--you
will likely get the wrong result.

No coding has been performed yet to handle `gits remote set-url` or
`gits branch --set-upstream`.  See `gits update-remote-url` for a
supported method to perform this operation.  Support could be added if
necessary.

You can have partial success, failure, and repositories on which the
operation was never tried and you must recover from such manually.
This is usually not very complicated.  See --keep-going.

Programs like gitk will not show the global system history.

Special care may be needed if one or more of the repositories is a
third party repository and you plan to have a complex branch/tag
management strategy, plan to do (public or private) development on the
third party repository, or might sometimes not want the absolute
latest code on the third party branch.  See the gitslave home page
for more information on workarounds.

The behavior when different branches have different slave repositories
associated with them and you checkout back and forth is probably not
ideal (nor are any of the options we have thought of completely
ideal).



=head1 FILES

=head2 .gitslave

The file containing the list of slave repositories (possibly in
relative form) and the directories relative to the master root where
they should be checked out.

The format of this file is:

"possibly-relative-repository-path" "top-level-checkout-relative-path"[ flags]

The flags, which are optional, currently can be the value "ifpresent"
which indicates that gits will only process this repository if the
top-level-checkout-relative-path is already present.

=head1 ENVIRONMENT

=head2 GITSLAVE

The GITSLAVE environment variable specifies alternate location(s) of the
.gitslave file.  Note that the .gitslave file must still exist even if it is
not used for this particular operation).  GITSLAVE can be a filename or a
list of filenames separated by comma and space; if a list of filenames is
specified, it has the same effect as if the files were concatenated.

An example of a list of filenames, GITSLAVE=".gitslave, .gitslave-extras"
would allow adding a supplemental list of slaves for unusual activity
(e.g. release tagging) to the normal list.

You can also use an alternate .gitslave file with just a subset of the
slave repositories when you don't want to run commands on all of them.

Note that if you are using recursive gitslave superprojects, the GITSLAVE
environment variable overrides .gitslave at the top-level only.  Only if the
alternate .gitslave file(s) #include .gitslave (or alternate) in recursive
superprojects will their slave repositories be included.

=head1 REQUIREMENTS

perl 5 (probably almost any version of perl 5)

git 1.6 or later (git 1.7 or later preferred)

Optionally uses Parallel::Iterator and Term::ProgressBar if available

=head1 AUTHOR

Seth Robertson

=head1 REPORTING BUGS

Report bugs to L<< http://sourceforge.net/projects/gitslave >>

=head1 COPYRIGHT

Copyright (c) 2008 Seth Robertson.  License is similar to the GNU
Lesser General Public License version 2.1, see LICENSE.TXT for more
details.

=head1 SEE ALSO

git(1), git-submodule(1), git-subtree(google)

Gitslave Home Page: L<< http://gitslave.sf.net >>
